/** * @ai-summary Unit tests for VehiclesService * @ai-context Tests business logic with mocked dependencies */ import { VehiclesService } from '../../domain/vehicles.service'; import { VehiclesRepository } from '../../data/vehicles.repository'; import { cacheService } from '../../../../core/config/redis'; import * as platformModule from '../../../platform'; // Mock dependencies jest.mock('../../data/vehicles.repository'); jest.mock('../../../../core/config/redis'); jest.mock('../../../platform', () => ({ getVehicleDataService: jest.fn(), getPool: jest.fn() })); const mockRepository = jest.mocked(VehiclesRepository); const mockCacheService = jest.mocked(cacheService); const mockGetVehicleDataService = jest.mocked(platformModule.getVehicleDataService); const mockGetPool = jest.mocked(platformModule.getPool); describe('VehiclesService', () => { let service: VehiclesService; let repositoryInstance: jest.Mocked; let vehicleDataServiceMock: { getYears: jest.Mock; getMakes: jest.Mock; getModels: jest.Mock; getTrims: jest.Mock; getEngines: jest.Mock; }; beforeEach(() => { jest.clearAllMocks(); vehicleDataServiceMock = { getYears: jest.fn(), getMakes: jest.fn(), getModels: jest.fn(), getTrims: jest.fn(), getEngines: jest.fn(), }; mockGetVehicleDataService.mockReturnValue(vehicleDataServiceMock as any); mockGetPool.mockReturnValue('mock-pool' as any); repositoryInstance = { create: jest.fn(), findByUserId: jest.fn(), findById: jest.fn(), findByUserAndVIN: jest.fn(), update: jest.fn(), softDelete: jest.fn(), } as any; mockRepository.mockImplementation(() => repositoryInstance); service = new VehiclesService(repositoryInstance); }); describe('dropdown data integration', () => { it('retrieves years from platform service', async () => { vehicleDataServiceMock.getYears.mockResolvedValue([2024, 2023]); const result = await service.getDropdownYears(); expect(mockGetVehicleDataService).toHaveBeenCalled(); expect(vehicleDataServiceMock.getYears).toHaveBeenCalledWith('mock-pool'); expect(result).toEqual([2024, 2023]); }); it('retrieves makes scoped to year', async () => { vehicleDataServiceMock.getMakes.mockResolvedValue([{ id: 10, name: 'Honda' }]); const result = await service.getDropdownMakes(2024); expect(vehicleDataServiceMock.getMakes).toHaveBeenCalledWith('mock-pool', 2024); expect(result).toEqual([{ id: 10, name: 'Honda' }]); }); it('retrieves models scoped to year and make', async () => { vehicleDataServiceMock.getModels.mockResolvedValue([{ id: 20, name: 'Civic' }]); const result = await service.getDropdownModels(2024, 10); expect(vehicleDataServiceMock.getModels).toHaveBeenCalledWith('mock-pool', 2024, 10); expect(result).toEqual([{ id: 20, name: 'Civic' }]); }); it('retrieves trims scoped to year, make, and model', async () => { vehicleDataServiceMock.getTrims.mockResolvedValue([{ id: 30, name: 'Sport' }]); const result = await service.getDropdownTrims(2024, 10, 20); expect(vehicleDataServiceMock.getTrims).toHaveBeenCalledWith('mock-pool', 2024, 20); expect(result).toEqual([{ id: 30, name: 'Sport' }]); }); it('retrieves engines scoped to selection', async () => { vehicleDataServiceMock.getEngines.mockResolvedValue([{ id: 40, name: '2.0L Turbo' }]); const result = await service.getDropdownEngines(2024, 10, 20, 30); expect(vehicleDataServiceMock.getEngines).toHaveBeenCalledWith('mock-pool', 2024, 20, 30); expect(result).toEqual([{ id: 40, name: '2.0L Turbo' }]); }); it('returns static transmission options', async () => { const result = await service.getDropdownTransmissions(2024, 10, 20); expect(result).toEqual([ { id: 1, name: 'Automatic' }, { id: 2, name: 'Manual' } ]); }); }); describe('createVehicle', () => { const mockVehicleData = { vin: '1HGBH41JXMN109186', nickname: 'My Car', color: 'Blue', odometerReading: 50000, }; const mockCreatedVehicle = { id: 'vehicle-id-123', userId: 'user-123', vin: '1HGBH41JXMN109186', make: undefined, model: undefined, year: undefined, nickname: 'My Car', color: 'Blue', licensePlate: undefined, odometerReading: 50000, isActive: true, deletedAt: undefined, createdAt: new Date('2024-01-01T00:00:00Z'), updatedAt: new Date('2024-01-01T00:00:00Z'), }; it('should create a vehicle with user-provided VIN', async () => { repositoryInstance.findByUserAndVIN.mockResolvedValue(null); repositoryInstance.create.mockResolvedValue(mockCreatedVehicle); mockCacheService.del.mockResolvedValue(undefined); const result = await service.createVehicle(mockVehicleData, 'user-123'); expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186'); expect(repositoryInstance.create).toHaveBeenCalledWith({ ...mockVehicleData, userId: 'user-123', make: undefined, model: undefined, }); expect(result.id).toBe('vehicle-id-123'); }); it('should reject invalid VIN format', async () => { const invalidVin = { ...mockVehicleData, vin: 'INVALID' }; await expect(service.createVehicle(invalidVin, 'user-123')).rejects.toThrow('Invalid VIN format'); }); it('should reject duplicate VIN for same user', async () => { repositoryInstance.findByUserAndVIN.mockResolvedValue(mockCreatedVehicle); await expect(service.createVehicle(mockVehicleData, 'user-123')).rejects.toThrow('Vehicle with this VIN already exists'); }); }); describe('getUserVehicles', () => { it('should return cached vehicles if available', async () => { const cachedVehicles = [{ id: 'vehicle-1', vin: '1HGBH41JXMN109186' }]; mockCacheService.get.mockResolvedValue(cachedVehicles); const result = await service.getUserVehicles('user-123'); expect(mockCacheService.get).toHaveBeenCalledWith('vehicles:user:user-123'); expect(result).toBe(cachedVehicles); expect(repositoryInstance.findByUserId).not.toHaveBeenCalled(); }); it('should fetch from database and cache if not cached', async () => { const mockVehicles = [ { id: 'vehicle-id-123', userId: 'user-123', vin: '1HGBH41JXMN109186', make: 'Honda', model: 'Civic', year: 2021, nickname: 'My Car', color: 'Blue', licensePlate: undefined, odometerReading: 50000, isActive: true, deletedAt: undefined, createdAt: new Date('2024-01-01T00:00:00Z'), updatedAt: new Date('2024-01-01T00:00:00Z'), } ]; mockCacheService.get.mockResolvedValue(null); repositoryInstance.findByUserId.mockResolvedValue(mockVehicles); mockCacheService.set.mockResolvedValue(undefined); const result = await service.getUserVehicles('user-123'); expect(repositoryInstance.findByUserId).toHaveBeenCalledWith('user-123'); expect(mockCacheService.set).toHaveBeenCalledWith('vehicles:user:user-123', expect.any(Array), 300); expect(result).toHaveLength(1); expect(result[0].id).toBe('vehicle-id-123'); }); }); describe('getVehicle', () => { const mockVehicle = { id: 'vehicle-id-123', userId: 'user-123', vin: '1HGBH41JXMN109186', make: 'Honda', model: 'Civic', year: 2021, nickname: 'My Car', color: 'Blue', licensePlate: undefined, odometerReading: 50000, isActive: true, deletedAt: undefined, createdAt: new Date('2024-01-01T00:00:00Z'), updatedAt: new Date('2024-01-01T00:00:00Z'), }; it('should return vehicle if found and owned by user', async () => { repositoryInstance.findById.mockResolvedValue(mockVehicle); const result = await service.getVehicle('vehicle-id-123', 'user-123'); expect(repositoryInstance.findById).toHaveBeenCalledWith('vehicle-id-123'); expect(result.id).toBe('vehicle-id-123'); }); it('should throw error if vehicle not found', async () => { repositoryInstance.findById.mockResolvedValue(null); await expect(service.getVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Vehicle not found'); }); it('should throw error if user is not owner', async () => { repositoryInstance.findById.mockResolvedValue({ ...mockVehicle, userId: 'other-user' }); await expect(service.getVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized'); }); }); describe('updateVehicle', () => { const mockVehicle = { id: 'vehicle-id-123', userId: 'user-123', vin: '1HGBH41JXMN109186', make: 'Honda', model: 'Civic', year: 2021, nickname: 'My Car', color: 'Blue', licensePlate: undefined, odometerReading: 50000, isActive: true, deletedAt: undefined, createdAt: new Date('2024-01-01T00:00:00Z'), updatedAt: new Date('2024-01-01T00:00:00Z'), }; it('should update vehicle successfully', async () => { const updateData = { nickname: 'Updated Car', color: 'Red' }; const updatedVehicle = { ...mockVehicle, ...updateData }; repositoryInstance.findById.mockResolvedValue(mockVehicle); repositoryInstance.update.mockResolvedValue(updatedVehicle); mockCacheService.del.mockResolvedValue(undefined); const result = await service.updateVehicle('vehicle-id-123', updateData, 'user-123'); expect(repositoryInstance.findById).toHaveBeenCalledWith('vehicle-id-123'); expect(repositoryInstance.update).toHaveBeenCalledWith('vehicle-id-123', updateData); expect(mockCacheService.del).toHaveBeenCalledWith('vehicles:user:user-123'); expect(result.nickname).toBe('Updated Car'); expect(result.color).toBe('Red'); }); it('should throw error if vehicle not found', async () => { repositoryInstance.findById.mockResolvedValue(null); await expect(service.updateVehicle('vehicle-id-123', {}, 'user-123')).rejects.toThrow('Vehicle not found'); }); it('should throw error if user is not owner', async () => { repositoryInstance.findById.mockResolvedValue({ ...mockVehicle, userId: 'other-user' }); await expect(service.updateVehicle('vehicle-id-123', {}, 'user-123')).rejects.toThrow('Unauthorized'); }); }); describe('deleteVehicle', () => { const mockVehicle = { id: 'vehicle-id-123', userId: 'user-123', vin: '1HGBH41JXMN109186', make: 'Honda', model: 'Civic', year: 2021, nickname: 'My Car', color: 'Blue', licensePlate: undefined, odometerReading: 50000, isActive: true, deletedAt: undefined, createdAt: new Date('2024-01-01T00:00:00Z'), updatedAt: new Date('2024-01-01T00:00:00Z'), }; it('should delete vehicle successfully', async () => { repositoryInstance.findById.mockResolvedValue(mockVehicle); repositoryInstance.softDelete.mockResolvedValue(true); mockCacheService.del.mockResolvedValue(undefined); await service.deleteVehicle('vehicle-id-123', 'user-123'); expect(repositoryInstance.findById).toHaveBeenCalledWith('vehicle-id-123'); expect(repositoryInstance.softDelete).toHaveBeenCalledWith('vehicle-id-123'); expect(mockCacheService.del).toHaveBeenCalledWith('vehicles:user:user-123'); }); it('should throw error if vehicle not found', async () => { repositoryInstance.findById.mockResolvedValue(null); await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Vehicle not found'); }); it('should throw error if user is not owner', async () => { repositoryInstance.findById.mockResolvedValue({ ...mockVehicle, userId: 'other-user' }); await expect(service.deleteVehicle('vehicle-id-123', 'user-123')).rejects.toThrow('Unauthorized'); }); }); });