diff --git a/.gitignore b/.gitignore index f5d0152..5d4b8e9 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,9 @@ temp/ # Database local # ====================== *.sqlite -*.db \ No newline at end of file +*.db + +# ====================== +# Files local storage +# ====================== +uploads/ \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index d9d427b..b13e24e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/mongoose": "^11.0.4", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.1.19", "@nestjs/platform-socket.io": "^11.1.17", "@nestjs/swagger": "^11.2.6", "@nestjs/websockets": "^11.1.17", @@ -24,6 +24,7 @@ "class-validator": "^0.15.1", "mongoose": "^9.3.1", "morgan": "^1.10.1", + "multer": "^2.1.1", "node-nlp": "^5.0.0-alpha.5", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -40,6 +41,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/morgan": "^1.9.10", + "@types/multer": "^2.1.0", "@types/node": "^22.10.7", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", @@ -2532,15 +2534,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.17", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", - "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", + "version": "11.1.19", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.19.tgz", + "integrity": "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", "multer": "2.1.1", - "path-to-regexp": "8.3.0", + "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, "funding": { @@ -2552,6 +2554,16 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@nestjs/platform-socket.io": { "version": "11.1.17", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.17.tgz", @@ -3775,6 +3787,16 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", diff --git a/backend/package.json b/backend/package.json index 9bf4ec0..f8201e5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,7 +27,7 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/mongoose": "^11.0.4", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-express": "^11.1.19", "@nestjs/platform-socket.io": "^11.1.17", "@nestjs/swagger": "^11.2.6", "@nestjs/websockets": "^11.1.17", @@ -36,6 +36,7 @@ "class-validator": "^0.15.1", "mongoose": "^9.3.1", "morgan": "^1.10.1", + "multer": "^2.1.1", "node-nlp": "^5.0.0-alpha.5", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -52,6 +53,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/morgan": "^1.9.10", + "@types/multer": "^2.1.0", "@types/node": "^22.10.7", "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f8e401e..847cd32 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -11,6 +11,7 @@ import { ChatModule } from './modules/chat/chat.module'; import { TriageModule } from './modules/triage/triage.module'; import { CategoryModule } from './modules/category/category.module'; import { TicketModule } from './modules/ticket/ticket.module'; +import { FileModule } from './modules/file/file.module'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { TicketModule } from './modules/ticket/ticket.module'; TriageModule, CategoryModule, TicketModule, + FileModule, ], controllers: [], providers: [], diff --git a/backend/src/modules/Messages/infra/message.schema.ts b/backend/src/modules/Messages/infra/message.schema.ts index 7e93bd7..4712b7c 100644 --- a/backend/src/modules/Messages/infra/message.schema.ts +++ b/backend/src/modules/Messages/infra/message.schema.ts @@ -19,6 +19,9 @@ export class Message { @Prop({ required: false }) readAt?: Date; // Controle pra saber se a mensagem foi lida + + @Prop({ type: [String], default: [] }) + fileIds?: string[]; // Arquivos anexados a mensagem } export const MessageSchema = SchemaFactory.createForClass(Message); diff --git a/backend/src/modules/Messages/presentation/message.gateway.ts b/backend/src/modules/Messages/presentation/message.gateway.ts index 6536469..c7faba7 100644 --- a/backend/src/modules/Messages/presentation/message.gateway.ts +++ b/backend/src/modules/Messages/presentation/message.gateway.ts @@ -66,6 +66,7 @@ export class MessageGateway senderId: data.senderId, content: data.content, isSystemMessage: data.isSystemMessage || false, + fileIds: data.fileIds || [], }); console.log(`Mensagem salva no banco com ID: ${mensagemSalva.id}`); diff --git a/backend/src/modules/chat/application/chat.file-message.spec.ts b/backend/src/modules/chat/application/chat.file-message.spec.ts new file mode 100644 index 0000000..96f2a64 --- /dev/null +++ b/backend/src/modules/chat/application/chat.file-message.spec.ts @@ -0,0 +1,192 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from './chat.service'; +import { UserRole } from '../../user/user.schema'; +import { NotFoundException } from '@nestjs/common'; + +import type { IChatRepository } from '../domain/chat.repository'; +import type { IMessageRepository } from '../../Messages/domain/message.repository'; + +/* =========================== + MOCKS +=========================== */ + +const mockChatRepository: jest.Mocked = { + create: jest.fn(), + findById: jest.fn(), + findByParticipant: jest.fn(), + findByTicketId: jest.fn(), + updateStatus: jest.fn(), +}; + +const mockMessageRepository: jest.Mocked = { + create: jest.fn(), + findByChatId: jest.fn(), +}; + +/* =========================== + CONSTANTES +=========================== */ + +const CHAT_ID = 'chat-001'; +const CLIENT_ID = 'user-001'; + +const mockChat = { + id: CHAT_ID, + ticketId: 'ticket-001', + clientId: CLIENT_ID, + agentId: 'agent-001', + groupId: 'group-001', + status: 'open', + createdAt: new Date(), +}; + +/* =========================== + TESTES +=========================== */ + +describe('ChatService — envio de mensagem com arquivo', () => { + let service: ChatService; + + beforeEach(async () => { + const module: TestingModule = + await Test.createTestingModule({ + providers: [ + ChatService, + { + provide: 'IChatRepository', + useValue: mockChatRepository, + }, + { + provide: 'IMessageRepository', + useValue: mockMessageRepository, + }, + ], + }).compile(); + + service = + module.get(ChatService); + + jest.clearAllMocks(); + }); + + /* =========================== + TESTE PRINCIPAL + =========================== */ + + it('should send message with attached file', async () => { + + const FILE_ID = 'file-001'; + + const mockSavedMessage = { + id: 'msg-001', + chatId: CHAT_ID, + senderId: CLIENT_ID, + content: 'Arquivo anexado', + fileIds: [FILE_ID], + isSystemMessage: false, + createdAt: new Date(), + }; + + /* Chat existe */ + mockChatRepository + .findById + .mockResolvedValue(mockChat as any); + + /* Mensagem salva */ + mockMessageRepository + .create + .mockResolvedValue(mockSavedMessage as any); + + const result = + await service.sendMessage( + CHAT_ID, + CLIENT_ID, + UserRole.CLIENT, + 'Arquivo anexado', + [FILE_ID], + ); + + expect(result) + .toEqual(mockSavedMessage); + + expect( + mockMessageRepository.create + ).toHaveBeenCalledWith({ + chatId: CHAT_ID, + senderId: CLIENT_ID, + content: 'Arquivo anexado', + isSystemMessage: false, + fileIds: [FILE_ID], + }); + }); + + /* =========================== + TESTE: SEM ARQUIVO + =========================== */ + + it('should send message without fileIds', async () => { + + const mockSavedMessage = { + id: 'msg-002', + chatId: CHAT_ID, + senderId: CLIENT_ID, + content: 'Mensagem normal', + fileIds: [], + isSystemMessage: false, + createdAt: new Date(), + }; + + mockChatRepository + .findById + .mockResolvedValue(mockChat as any); + + mockMessageRepository + .create + .mockResolvedValue(mockSavedMessage as any); + + const result = + await service.sendMessage( + CHAT_ID, + CLIENT_ID, + UserRole.CLIENT, + 'Mensagem normal', + ); + + expect(result) + .toEqual(mockSavedMessage); + + expect( + mockMessageRepository.create + ).toHaveBeenCalledWith({ + chatId: CHAT_ID, + senderId: CLIENT_ID, + content: 'Mensagem normal', + isSystemMessage: false, + fileIds: [], + }); + }); + + /* =========================== + TESTE: CHAT NÃO EXISTE + =========================== */ + + it('should throw NotFoundException when chat does not exist', async () => { + + mockChatRepository + .findById + .mockResolvedValue(null); + + await expect( + service.sendMessage( + 'invalid-chat', + CLIENT_ID, + UserRole.CLIENT, + 'Teste', + ['file-001'], + ) + ).rejects.toThrow( + NotFoundException, + ); + }); + +}); \ No newline at end of file diff --git a/backend/src/modules/chat/application/chat.service.spec.ts b/backend/src/modules/chat/application/chat.service.spec.ts index b423c41..90f6eb9 100644 --- a/backend/src/modules/chat/application/chat.service.spec.ts +++ b/backend/src/modules/chat/application/chat.service.spec.ts @@ -182,6 +182,7 @@ describe('ChatService', () => { senderId: CLIENT_ID, content: 'Olá, preciso de ajuda!', isSystemMessage: false, + fileIds: [], }); }); diff --git a/backend/src/modules/chat/application/chat.service.ts b/backend/src/modules/chat/application/chat.service.ts index 9c8651b..c2f2a6f 100644 --- a/backend/src/modules/chat/application/chat.service.ts +++ b/backend/src/modules/chat/application/chat.service.ts @@ -45,6 +45,7 @@ export class ChatService { senderId: string, senderRole: UserRole, content: string, + fileIds?: string[], ): Promise { const chat = await this.chatRepository.findById(chatId); if (!chat) { @@ -63,6 +64,7 @@ export class ChatService { senderId, content, isSystemMessage: false, + fileIds: fileIds || [], }); } diff --git a/backend/src/modules/file/application/file.service.ts b/backend/src/modules/file/application/file.service.ts new file mode 100644 index 0000000..cbcd5ca --- /dev/null +++ b/backend/src/modules/file/application/file.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { FileDocument } from '../infra/schemas/file.schema'; +import { FileEntity } from '../domain/file.entity'; +import { LocalStorage } from '../infra/storage/local.storage'; + +@Injectable() +export class FileService { + constructor( + @InjectModel('File') + private readonly fileModel: + Model, + + private readonly storage: + LocalStorage + ) {} + + async createFile( + file: Express.Multer.File, + subFolder: string, + uploadedBy?: string + ): Promise { + const fileData = + this.storage.save( + file, + subFolder, + uploadedBy + ); + + const newFile = + new this.fileModel({ + filename: fileData.filename, + originalname: + fileData.originalname, + mimetype: + fileData.mimetype, + size: + fileData.size, + path: + fileData.path, + uploadedBy: + fileData.uploadedBy + }); + + const savedFile = + await newFile.save(); + return this._mapToEntity( + savedFile + ); + } + + private _mapToEntity( + file: any + ): FileEntity { + return { + id: file._id, + filename: file.filename, + originalname: + file.originalname, + mimetype: + file.mimetype, + size: file.size, + path: file.path, + uploadedBy: + file.uploadedBy, + createdAt: + file.createdAt + }; + } +} \ No newline at end of file diff --git a/backend/src/modules/file/config/multer.config.ts b/backend/src/modules/file/config/multer.config.ts new file mode 100644 index 0000000..0abf749 --- /dev/null +++ b/backend/src/modules/file/config/multer.config.ts @@ -0,0 +1,24 @@ +import { diskStorage } from 'multer'; +import { extname } from 'path'; + +export const multerConfig = { + storage: diskStorage({ + destination: './uploads', + + filename: (req, file, cb) => { + const uniqueSuffix = + Date.now() + + '-' + + Math.round(Math.random() * 1e9); + const extension = + extname(file.originalname); + const filename = + `${uniqueSuffix}${extension}`; + cb(null, filename); + } + }), + + limits: { + fileSize: 10 * 1024 * 1024 + } +}; \ No newline at end of file diff --git a/backend/src/modules/file/domain/file.entity.ts b/backend/src/modules/file/domain/file.entity.ts new file mode 100644 index 0000000..21dd660 --- /dev/null +++ b/backend/src/modules/file/domain/file.entity.ts @@ -0,0 +1,10 @@ +export class FileEntity { + id?: string; + filename: string; + originalname: string; + mimetype: string; + size: number; + path: string; + uploadedBy?: string; + createdAt?: Date; +} \ No newline at end of file diff --git a/backend/src/modules/file/file.module.ts b/backend/src/modules/file/file.module.ts new file mode 100644 index 0000000..caa490f --- /dev/null +++ b/backend/src/modules/file/file.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { FileController } from './presentation/file.controller'; +import { FileService } from './application/file.service'; +import { FileSchema } from './infra/schemas/file.schema'; +import { LocalStorage } from './infra/storage/local.storage'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: 'File', + schema: FileSchema + } + ]) + ], + controllers: [ + FileController + ], + providers: [ + FileService, + LocalStorage + ], + exports: [ + FileService + ] +}) +export class FileModule {} \ No newline at end of file diff --git a/backend/src/modules/file/infra/schemas/file.schema.ts b/backend/src/modules/file/infra/schemas/file.schema.ts new file mode 100644 index 0000000..6ffdc95 --- /dev/null +++ b/backend/src/modules/file/infra/schemas/file.schema.ts @@ -0,0 +1,25 @@ +import { Schema, Document } from 'mongoose'; + +export interface FileDocument extends Document { + filename: string; + originalname: string; + mimetype: string; + size: number; + path: string; + uploadedBy?: string; + createdAt?: Date; +} + +export const FileSchema = new Schema( + { + filename: { type: String, required: true }, + originalname: { type: String, required: true }, + mimetype: { type: String, required: true }, + size: { type: Number, required: true }, + path: { type: String, required: true }, + uploadedBy: { type: String } + }, + { + timestamps: true + } +); \ No newline at end of file diff --git a/backend/src/modules/file/infra/storage/local.storage.ts b/backend/src/modules/file/infra/storage/local.storage.ts new file mode 100644 index 0000000..efa2691 --- /dev/null +++ b/backend/src/modules/file/infra/storage/local.storage.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { FileEntity } from '../../domain/file.entity'; +import { existsSync, mkdirSync, renameSync } from 'fs'; +import { join } from 'path'; + +@Injectable() +export class LocalStorage { + + save( + file: Express.Multer.File, + subFolder: string, + uploadedBy?: string + ): FileEntity { + const uploadPath = + join( + 'uploads', + subFolder + ); + if (!existsSync(uploadPath)) { + mkdirSync( + uploadPath, + { recursive: true } + ); + } + const newPath = + join( + uploadPath, file.filename + ); + renameSync( + file.path, newPath + ); + return { + id: undefined, + filename: file.filename, + originalname: file.originalname, + mimetype: file.mimetype, + size: file.size, + path: file.path, + uploadedBy, + createdAt: new Date() + }; + } +} \ No newline at end of file diff --git a/backend/src/modules/file/presentation/dtos/uploadChatFileParamsDTO.ts b/backend/src/modules/file/presentation/dtos/uploadChatFileParamsDTO.ts new file mode 100644 index 0000000..968b02b --- /dev/null +++ b/backend/src/modules/file/presentation/dtos/uploadChatFileParamsDTO.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class UploadChatFileParamsDTO { + + @ApiProperty({ + example: '8156f5c6-9b82-4be9-881e-641082d0bb56' + }) + @IsString() + chatId: string; + +} \ No newline at end of file diff --git a/backend/src/modules/file/presentation/dtos/uploadFileDTO.ts b/backend/src/modules/file/presentation/dtos/uploadFileDTO.ts new file mode 100644 index 0000000..87243a5 --- /dev/null +++ b/backend/src/modules/file/presentation/dtos/uploadFileDTO.ts @@ -0,0 +1 @@ +export class UploadFileDTO {} \ No newline at end of file diff --git a/backend/src/modules/file/presentation/file.controller.ts b/backend/src/modules/file/presentation/file.controller.ts new file mode 100644 index 0000000..87f775d --- /dev/null +++ b/backend/src/modules/file/presentation/file.controller.ts @@ -0,0 +1,89 @@ +import { Controller, Post, UploadedFile, UseInterceptors, UseGuards, Body, Req, Param } from '@nestjs/common'; +import { ApiBearerAuth, ApiConsumes, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { JwtGuard } from '../../auth/guards/jwt.guard'; +import { RolesGuard } from '../../auth/guards/roles.guard'; +import { Roles } from '../../auth/guards/roles.decorator'; +import { UserRole } from '../../user/user.schema'; +import { FileService } from '../application/file.service'; +import { UploadFileDTO } from './dtos/uploadFileDTO'; +import { multerConfig } from '../config/multer.config'; +import type { Express, Request } from 'express'; +import { UploadChatFileParamsDTO } from './dtos/uploadChatFileParamsDTO'; + +@ApiTags('Files') +@ApiBearerAuth() +@Controller('files') +export class FileController { + constructor( + private fileService: FileService + ) { } + + @Post() + @UseGuards(JwtGuard, RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPPORT, UserRole.CLIENT) + @ApiOperation({ summary: 'Upload de arquivo' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary' + } + } + } + }) + @UseInterceptors( + FileInterceptor('file', multerConfig) + ) + async uploadFile( + @UploadedFile() + file: Express.Multer.File, + @Body() + dto: UploadFileDTO, + @Req() + req: Request & { user: { id: string } } + ) { + const userId = req.user.id; + return this.fileService.createFile(file, userId); + } + + @Post('chat/:chatId') + @UseGuards(JwtGuard, RolesGuard) + @Roles(UserRole.ADMIN, UserRole.SUPPORT, UserRole.CLIENT) + @UseInterceptors( + FileInterceptor( + 'file', + multerConfig + ) + ) + @ApiOperation({ + summary: 'Upload de arquivo para chat' + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary' + } + } + } + }) + async uploadChatFile( + @UploadedFile() + file: Express.Multer.File, + @Req() + req: Request & { user: { id: string } }, + @Param() + params: UploadChatFileParamsDTO + ) { + const userId = req.user.id; + const subFolder = `chat/${params.chatId}`; + return this.fileService.createFile(file, subFolder, userId); + } +} \ No newline at end of file diff --git a/backend/src/modules/shared/domain/ticket-category.enum.ts b/backend/src/modules/shared/domain/ticket-category.enum.ts deleted file mode 100644 index 285371b..0000000 --- a/backend/src/modules/shared/domain/ticket-category.enum.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum TicketCategory { - WEB_APP = 'WEB_APP', - IA = 'ARTIFICIAL_INTELLIGENCE', - BI = 'BUSINESS_INTELLIGENCE', - IOT = 'INTERNET_OF_THINGS', - OTHER = 'OTHER', -} diff --git a/backend/src/modules/triage/domain/triage-result.type.ts b/backend/src/modules/triage/domain/triage-result.type.ts deleted file mode 100644 index c2f2f78..0000000 --- a/backend/src/modules/triage/domain/triage-result.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type TriageResult = { - category: string; - confidence: number; - source: 'rule' | 'nlp' | 'fallback'; -}; diff --git a/backend/src/modules/triage/presentation/triage.controller.spec.ts b/backend/src/modules/triage/presentation/triage.controller.spec.ts index 59d1eed..dd6f335 100644 --- a/backend/src/modules/triage/presentation/triage.controller.spec.ts +++ b/backend/src/modules/triage/presentation/triage.controller.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TriageController } from './triage.controller'; import { TriageService } from '../application/triage.service'; -import { TicketCategory } from '../../shared/domain/ticket-category.enum'; +import { Category } from '../domain/category.entity'; describe('TriageController', () => { let controller: TriageController; @@ -12,71 +12,121 @@ describe('TriageController', () => { }; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [TriageController], - providers: [ - { - provide: TriageService, - useValue: mockTriageService, - }, - ], - }).compile(); - - controller = module.get(TriageController); - triageService = module.get(TriageService); + const module: TestingModule = + await Test.createTestingModule({ + controllers: [TriageController], + providers: [ + { + provide: TriageService, + useValue: mockTriageService, + }, + ], + }).compile(); + + controller = + module.get( + TriageController + ); + + triageService = + module.get(TriageService); }); afterEach(() => { jest.clearAllMocks(); }); - it('deve classificar corretamente uma descrição', async () => { - const mockResponse = { - value: TicketCategory.WEB_APP, - confidence: 0.95, - source: 'rule', - }; - - triageService.classify.mockResolvedValue(mockResponse as any); - - const body = { - description: 'site não abre', - }; - - const result = await controller.classify(body); - - expect(triageService.classify).toHaveBeenCalledWith(body.description); - expect(result).toEqual(mockResponse); - }); - - it('deve retornar fallback quando service retornar OTHER', async () => { - const mockResponse = { - value: TicketCategory.OTHER, - confidence: 0.5, - source: 'fallback', - }; - - triageService.classify.mockResolvedValue(mockResponse as any); - - const body = { - description: 'qualquer coisa aleatória', - }; - - const result = await controller.classify(body); - - expect(result.value).toBe(TicketCategory.OTHER); - expect(result.source).toBe('fallback'); - }); - - it('deve chamar o service apenas uma vez', async () => { - triageService.classify.mockResolvedValue({ - value: TicketCategory.BI, - confidence: 0.9, - source: 'rule', - } as any); - - await controller.classify({ description: 'erro no dashboard' }); - - expect(triageService.classify).toHaveBeenCalledTimes(1); - }); -}); + it( + 'deve classificar corretamente uma descrição', + async () => { + + const mockResponse = + new Category( + 'WEB_APP', + 0.95, + 'rule' + ); + + triageService.classify + .mockResolvedValue( + mockResponse + ); + + const body = { + description: 'site não abre', + }; + + const result = + await controller.classify( + body + ); + + expect( + triageService.classify + ).toHaveBeenCalledWith( + body.description + ); + + expect(result) + .toEqual(mockResponse); + } + ); + + it( + 'deve retornar fallback quando categoria for OTHER', + async () => { + + const mockResponse = + new Category( + 'OTHER', + 0.5, + 'fallback' + ); + + triageService.classify + .mockResolvedValue( + mockResponse + ); + + const body = { + description: + 'qualquer coisa aleatória', + }; + + const result = + await controller.classify( + body + ); + + expect(result.category) + .toBe('OTHER'); + + expect(result.source) + .toBe('fallback'); + } + ); + + it( + 'deve chamar o service apenas uma vez', + async () => { + + triageService.classify + .mockResolvedValue( + new Category( + 'BI', + 0.9, + 'rule' + ) + ); + + await controller.classify({ + description: + 'erro no dashboard', + }); + + expect( + triageService.classify + ).toHaveBeenCalledTimes(1); + } + ); +}); \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 098d432..18a3784 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -13,6 +13,7 @@ "target": "ES2023", "sourceMap": true, "outDir": "./dist", + "rootDir": ".", // "baseUrl": "./", "incremental": true, "skipLibCheck": true,