Files
motovaultpro/backend/src/features/vehicles/tests/unit/vehicles.service.test.ts
2025-12-15 18:19:55 -06:00

350 lines
12 KiB
TypeScript

/**
* @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<VehiclesRepository>;
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');
});
});
});