diff --git a/src/data-consult/__tests__/data-consult.service.spec.ts b/src/data-consult/__tests__/data-consult.service.spec.ts new file mode 100644 index 0000000..8b44134 --- /dev/null +++ b/src/data-consult/__tests__/data-consult.service.spec.ts @@ -0,0 +1,314 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { DataConsultService } from '../data-consult.service'; +import { DataConsultRepository } from '../data-consult.repository'; +import { StoreDto } from '../dto/store.dto'; +import { SellerDto } from '../dto/seller.dto'; +import { ILogger } from '../../Log/ILogger'; +import { IRedisClient } from '../../core/configs/cache/IRedisClient'; +import { RedisClientToken } from '../../core/configs/cache/redis-client.adapter.provider'; +import { DataSource } from 'typeorm'; +import { DATA_SOURCE } from '../../core/constants'; + +describe('DataConsultService - stores', () => { + let service: DataConsultService; + let mockRepository: jest.Mocked; + let mockLogger: jest.Mocked; + let mockRedisClient: jest.Mocked; + let mockDataSource: jest.Mocked; + + beforeEach(async () => { + mockRepository = { + findStores: jest.fn(), + findSellers: jest.fn(), + } as any; + + mockLogger = { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + } as any; + + mockRedisClient = { + get: jest.fn(), + set: jest.fn(), + } as any; + + mockDataSource = {} as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DataConsultService, + { + provide: DataConsultRepository, + useValue: mockRepository, + }, + { + provide: RedisClientToken, + useValue: mockRedisClient, + }, + { + provide: 'LoggerService', + useValue: mockLogger, + }, + { + provide: DATA_SOURCE, + useValue: mockDataSource, + }, + ], + }).compile(); + + service = module.get(DataConsultService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('stores - Tests that expose problems', () => { + /** + * NOTA: Estes testes identificam problemas no método stores. + * + * PROBLEMAS IDENTIFICADOS: + * 1. Não valida se os dados retornados têm todas as propriedades necessárias + * 2. Não valida se o resultado do repository é um array válido + * 3. Permite criar StoreDto com propriedades undefined + * 4. Não trata o caso onde o repository retorna dados incompletos + */ + + it('should validate that all stores have required properties (id, name, store)', async () => { + /** + * Cenário: Repository retorna dados incompletos (faltando propriedades). + * Problema: StoreDto é criado com propriedades undefined. + * Solução esperada: Validar dados antes de criar StoreDto ou lançar exceção. + */ + mockRepository.findStores.mockResolvedValue([ + { id: '001', name: 'Loja 1' }, // falta propriedade 'store' + { id: '002', store: '002 - Loja 2' }, // falta propriedade 'name' + { id: '003', name: 'Loja 3', store: '003 - Loja 3' }, // completo + ] as any); + + const result = await service.stores(); + + // Este teste falha porque o método atual não valida as propriedades + // e permite criar StoreDto com propriedades undefined + result.forEach(store => { + expect(store.id).toBeDefined(); + expect(store.name).toBeDefined(); + expect(store.store).toBeDefined(); + }); + }); + + it('should handle null or undefined result from repository', async () => { + /** + * Cenário: Repository retorna null ou undefined. + * Problema: Método tenta fazer map em null/undefined causando erro. + * Solução esperada: Validar se o resultado é um array antes de fazer map. + */ + mockRepository.findStores.mockResolvedValue(null as any); + + await expect(service.stores()).rejects.toThrow(); + }); + + it('should validate that repository result is an array', async () => { + /** + * Cenário: Repository retorna um objeto em vez de array. + * Problema: Método tenta fazer map em objeto causando comportamento inesperado. + * Solução esperada: Validar se o resultado é um array. + */ + mockRepository.findStores.mockResolvedValue({ id: '001', name: 'Loja 1' } as any); + + const result = await service.stores(); + + // Este teste falha porque o método não valida se o resultado é um array + expect(Array.isArray(result)).toBe(true); + }); + + it('should validate that required properties are not empty strings', async () => { + /** + * Cenário: Repository retorna dados com propriedades vazias. + * Problema: StoreDto é criado com strings vazias, o que pode causar problemas. + * Solução esperada: Validar se as propriedades são strings não vazias. + */ + mockRepository.findStores.mockResolvedValue([ + { id: '', name: 'Loja 1', store: '001 - Loja 1' }, // id vazio + { id: '002', name: '', store: '002 - Loja 2' }, // name vazio + { id: '003', name: 'Loja 3', store: '' }, // store vazio + ] as any); + + const result = await service.stores(); + + // Este teste falha porque o método não valida se as propriedades são não vazias + result.forEach(store => { + expect(store.id).not.toBe(''); + expect(store.name).not.toBe(''); + expect(store.store).not.toBe(''); + }); + }); + + it('should log error when repository throws exception', async () => { + /** + * Cenário: Repository lança uma exceção. + * Problema: Método deve logar o erro antes de lançar HttpException. + * Solução esperada: Logger.error deve ser chamado com a mensagem correta. + */ + const repositoryError = new Error('Database connection failed'); + mockRepository.findStores.mockRejectedValue(repositoryError); + + await expect(service.stores()).rejects.toThrow(HttpException); + + expect(mockLogger.error).toHaveBeenCalledWith('Erro ao buscar lojas', repositoryError); + }); + }); +}); + +describe('DataConsultService - sellers', () => { + let service: DataConsultService; + let mockRepository: jest.Mocked; + let mockLogger: jest.Mocked; + let mockRedisClient: jest.Mocked; + let mockDataSource: jest.Mocked; + + beforeEach(async () => { + mockRepository = { + findSellers: jest.fn(), + } as any; + + mockLogger = { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + } as any; + + mockRedisClient = { + get: jest.fn().mockResolvedValue(null), + set: jest.fn().mockResolvedValue(undefined), + } as any; + + mockDataSource = {} as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DataConsultService, + { + provide: DataConsultRepository, + useValue: mockRepository, + }, + { + provide: RedisClientToken, + useValue: mockRedisClient, + }, + { + provide: 'LoggerService', + useValue: mockLogger, + }, + { + provide: DATA_SOURCE, + useValue: mockDataSource, + }, + ], + }).compile(); + + service = module.get(DataConsultService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('sellers - Tests that expose problems', () => { + /** + * NOTA: Estes testes identificam problemas no método sellers. + * + * PROBLEMAS IDENTIFICADOS: + * 1. Não valida se os dados retornados têm todas as propriedades necessárias + * 2. Não valida se o resultado do repository é um array válido + * 3. Permite criar SellerDto com propriedades undefined + * 4. Não trata o caso onde o repository retorna dados incompletos + */ + + it('should validate that all sellers have required properties (id, name)', async () => { + /** + * Cenário: Repository retorna dados incompletos (faltando propriedades). + * Problema: SellerDto é criado com propriedades undefined. + * Solução esperada: Validar dados antes de criar SellerDto ou lançar exceção. + */ + mockRepository.findSellers.mockResolvedValue([ + { id: '001' }, // falta propriedade 'name' + { name: 'Vendedor 2' }, // falta propriedade 'id' + { id: '003', name: 'Vendedor 3' }, // completo + ] as any); + + const result = await service.sellers(); + + // Este teste falha porque o método atual não valida as propriedades + // e permite criar SellerDto com propriedades undefined + result.forEach(seller => { + expect(seller.id).toBeDefined(); + expect(seller.name).toBeDefined(); + }); + }); + + it('should handle null or undefined result from repository', async () => { + /** + * Cenário: Repository retorna null ou undefined. + * Problema: Método tenta fazer map em null/undefined causando erro. + * Solução esperada: Validar se o resultado é um array antes de fazer map. + */ + mockRepository.findSellers.mockResolvedValue(null as any); + + await expect(service.sellers()).rejects.toThrow(); + }); + + it('should validate that repository result is an array', async () => { + /** + * Cenário: Repository retorna um objeto em vez de array. + * Problema: Método tenta fazer map em objeto causando comportamento inesperado. + * Solução esperada: Validar se o resultado é um array. + */ + mockRepository.findSellers.mockResolvedValue({ id: '001', name: 'Vendedor 1' } as any); + + const result = await service.sellers(); + + // Este teste falha porque o método não valida se o resultado é um array + expect(Array.isArray(result)).toBe(true); + }); + + it('should validate that required properties are not empty strings', async () => { + /** + * Cenário: Repository retorna dados com propriedades vazias. + * Problema: SellerDto é criado com strings vazias, o que pode causar problemas. + * Solução esperada: Validar se as propriedades são strings não vazias. + */ + mockRepository.findSellers.mockResolvedValue([ + { id: '', name: 'Vendedor 1' }, // id vazio + { id: '002', name: '' }, // name vazio + ] as any); + + const result = await service.sellers(); + + // Este teste falha porque o método não valida se as propriedades são não vazias + result.forEach(seller => { + expect(seller.id).not.toBe(''); + expect(seller.name).not.toBe(''); + }); + }); + + it('should log error when repository throws exception', async () => { + /** + * Cenário: Repository lança uma exceção. + * Problema: Método deve logar o erro antes de lançar HttpException. + * Solução esperada: Logger.error deve ser chamado com a mensagem correta. + */ + const repositoryError = new Error('Database connection failed'); + mockRepository.findSellers.mockRejectedValue(repositoryError); + + await expect(service.sellers()).rejects.toThrow(HttpException); + + expect(mockLogger.error).toHaveBeenCalledWith('Erro ao buscar vendedores', repositoryError); + }); + }); +}); + diff --git a/src/data-consult/data-consult.service.ts b/src/data-consult/data-consult.service.ts index c359a74..4d698ce 100644 --- a/src/data-consult/data-consult.service.ts +++ b/src/data-consult/data-consult.service.ts @@ -35,35 +35,65 @@ export class DataConsultService { @Inject(DATA_SOURCE) private readonly dataSource: DataSource ) {} - /** - * Obter todas as lojas - * @returns Array de StoreDto - */ async stores(): Promise { this.logger.log('Buscando todas as lojas'); try { const stores = await this.repository.findStores(); - return stores.map(store => new StoreDto(store)); + + if (stores === null || stores === undefined) { + throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); + } + + const storesArray = Array.isArray(stores) ? stores : [stores]; + + return storesArray + .filter(store => { + if (!store || typeof store !== 'object') { + return false; + } + const hasId = store.id !== undefined && store.id !== null && store.id !== ''; + const hasName = store.name !== undefined && store.name !== null && store.name !== ''; + const hasStore = store.store !== undefined && store.store !== null && store.store !== ''; + return hasId && hasName && hasStore; + }) + .map(store => new StoreDto(store)); } catch (error) { this.logger.error('Erro ao buscar lojas', error); throw new HttpException('Erro ao buscar lojas', HttpStatus.INTERNAL_SERVER_ERROR); } } - /** - * Obter todos os vendedores - * @returns Array de SellerDto - */ async sellers(): Promise { this.logger.log('Buscando vendedores com cache Redis...'); try { - return getOrSetCache( + return await getOrSetCache( this.redisClient, this.SELLERS_CACHE_KEY, this.SELLERS_TTL, async () => { - const sellers = await this.repository.findSellers(); - return sellers.map(seller => new SellerDto(seller)); + try { + const sellers = await this.repository.findSellers(); + + if (sellers === null || sellers === undefined) { + throw new HttpException('Resultado inválido do repositório', HttpStatus.INTERNAL_SERVER_ERROR); + } + + const sellersArray = Array.isArray(sellers) ? sellers : [sellers]; + + return sellersArray + .filter(seller => { + if (!seller || typeof seller !== 'object') { + return false; + } + const hasId = seller.id !== undefined && seller.id !== null && seller.id !== ''; + const hasName = seller.name !== undefined && seller.name !== null && seller.name !== ''; + return hasId && hasName; + }) + .map(seller => new SellerDto(seller)); + } catch (error) { + this.logger.error('Erro ao buscar vendedores', error); + throw error; + } } ); } catch (error) { @@ -72,9 +102,6 @@ export class DataConsultService { } } - /** - * @returns Array de BillingDto - */ async billings(): Promise { this.logger.log('Buscando informações de faturamento'); try { @@ -86,11 +113,6 @@ export class DataConsultService { } } - /** - * Obter clientes filtrados por termo de pesquisa - * @param filter - Termo de pesquisa para filtrar clientes - * @returns Array de CustomerDto - */ async customers(filter: string): Promise { this.logger.log(`Buscando clientes com filtro: ${filter}`); try { @@ -105,11 +127,6 @@ export class DataConsultService { } } - /** - * Obter produtos filtrados por termo de pesquisa - * @param filter - Termo de pesquisa para filtrar produtos - * @returns Array de ProductDto - */ async products(filter: string): Promise { this.logger.log(`Buscando produtos com filtro: ${filter}`); try { @@ -142,10 +159,6 @@ export class DataConsultService { } } - /** - * Obter todas as transportadoras cadastradas - * @returns Array de CarrierDto - */ async getAllCarriers(): Promise { this.logger.log('Buscando todas as transportadoras'); try { @@ -168,11 +181,6 @@ export class DataConsultService { } } - /** - * Obter transportadoras por período de data - * @param query - Filtros de data e filial - * @returns Array de CarrierDto - */ async getCarriersByDate(query: FindCarriersDto): Promise { this.logger.log(`Buscando transportadoras por período: ${JSON.stringify(query)}`); try { @@ -189,11 +197,6 @@ export class DataConsultService { } } - /** - * Obter transportadoras de um pedido específico - * @param orderId - ID do pedido - * @returns Array de CarrierDto - */ async getOrderCarriers(orderId: number): Promise { this.logger.log(`Buscando transportadoras do pedido: ${orderId}`); try { @@ -209,10 +212,6 @@ export class DataConsultService { } } - /** - * Obter todas as regiões cadastradas - * @returns Array de RegionDto - */ async getRegions(): Promise { this.logger.log('Buscando todas as regiões'); try {