MVP Build
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* @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 { vpicClient } from '../../external/vpic/vpic.client';
|
||||
import { cacheService } from '../../../../core/config/redis';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../data/vehicles.repository');
|
||||
jest.mock('../../external/vpic/vpic.client');
|
||||
jest.mock('../../../../core/config/redis');
|
||||
|
||||
const mockRepository = jest.mocked(VehiclesRepository);
|
||||
const mockVpicClient = jest.mocked(vpicClient);
|
||||
const mockCacheService = jest.mocked(cacheService);
|
||||
|
||||
describe('VehiclesService', () => {
|
||||
let service: VehiclesService;
|
||||
let repositoryInstance: jest.Mocked<VehiclesRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
repositoryInstance = {
|
||||
create: jest.fn(),
|
||||
findByUserId: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findByUserAndVIN: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
cacheVINDecode: jest.fn(),
|
||||
getVINFromCache: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockRepository.mockImplementation(() => repositoryInstance);
|
||||
service = new VehiclesService(repositoryInstance);
|
||||
});
|
||||
|
||||
describe('createVehicle', () => {
|
||||
const mockVehicleData = {
|
||||
vin: '1HGBH41JXMN109186',
|
||||
nickname: 'My Car',
|
||||
color: 'Blue',
|
||||
odometerReading: 50000,
|
||||
};
|
||||
|
||||
const mockVinDecodeResult = {
|
||||
make: 'Honda',
|
||||
model: 'Civic',
|
||||
year: 2021,
|
||||
engineType: '2.0L',
|
||||
bodyType: 'Sedan',
|
||||
rawData: [],
|
||||
};
|
||||
|
||||
const mockCreatedVehicle = {
|
||||
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 create a vehicle with VIN decoding', async () => {
|
||||
repositoryInstance.findByUserAndVIN.mockResolvedValue(null);
|
||||
mockVpicClient.decodeVIN.mockResolvedValue(mockVinDecodeResult);
|
||||
repositoryInstance.create.mockResolvedValue(mockCreatedVehicle);
|
||||
repositoryInstance.cacheVINDecode.mockResolvedValue(undefined);
|
||||
mockCacheService.del.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.createVehicle(mockVehicleData, 'user-123');
|
||||
|
||||
expect(repositoryInstance.findByUserAndVIN).toHaveBeenCalledWith('user-123', '1HGBH41JXMN109186');
|
||||
expect(mockVpicClient.decodeVIN).toHaveBeenCalledWith('1HGBH41JXMN109186');
|
||||
expect(repositoryInstance.create).toHaveBeenCalledWith({
|
||||
...mockVehicleData,
|
||||
userId: 'user-123',
|
||||
make: 'Honda',
|
||||
model: 'Civic',
|
||||
year: 2021,
|
||||
});
|
||||
expect(repositoryInstance.cacheVINDecode).toHaveBeenCalledWith('1HGBH41JXMN109186', mockVinDecodeResult);
|
||||
expect(result.id).toBe('vehicle-id-123');
|
||||
expect(result.make).toBe('Honda');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('should handle VIN decode failure gracefully', async () => {
|
||||
repositoryInstance.findByUserAndVIN.mockResolvedValue(null);
|
||||
mockVpicClient.decodeVIN.mockResolvedValue(null);
|
||||
repositoryInstance.create.mockResolvedValue({ ...mockCreatedVehicle, make: undefined, model: undefined, year: undefined });
|
||||
mockCacheService.del.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.createVehicle(mockVehicleData, 'user-123');
|
||||
|
||||
expect(repositoryInstance.create).toHaveBeenCalledWith({
|
||||
...mockVehicleData,
|
||||
userId: 'user-123',
|
||||
make: undefined,
|
||||
model: undefined,
|
||||
year: undefined,
|
||||
});
|
||||
expect(result.make).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user