MVP Build
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @ai-summary Integration tests for vehicles API endpoints
|
||||
* @ai-context Tests complete request/response cycle with test database
|
||||
*/
|
||||
|
||||
import request from 'supertest';
|
||||
import { app } from '../../../../app';
|
||||
import { pool } from '../../../../core/config/database';
|
||||
import { cacheService } from '../../../../core/config/redis';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Mock auth middleware to bypass JWT validation in tests
|
||||
jest.mock('../../../../core/security/auth.middleware', () => ({
|
||||
authMiddleware: (req: any, _res: any, next: any) => {
|
||||
req.user = { sub: 'test-user-123' };
|
||||
next();
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock external VIN decoder
|
||||
jest.mock('../../external/vpic/vpic.client', () => ({
|
||||
vpicClient: {
|
||||
decodeVIN: jest.fn().mockResolvedValue({
|
||||
make: 'Honda',
|
||||
model: 'Civic',
|
||||
year: 2021,
|
||||
engineType: '2.0L',
|
||||
bodyType: 'Sedan',
|
||||
rawData: []
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
describe('Vehicles Integration Tests', () => {
|
||||
beforeAll(async () => {
|
||||
// Run the vehicles migration directly using the migration file
|
||||
const migrationFile = join(__dirname, '../../migrations/001_create_vehicles_tables.sql');
|
||||
const migrationSQL = readFileSync(migrationFile, 'utf-8');
|
||||
await pool.query(migrationSQL);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test database
|
||||
await pool.query('DROP TABLE IF EXISTS vehicles CASCADE');
|
||||
await pool.query('DROP TABLE IF EXISTS vin_cache CASCADE');
|
||||
await pool.query('DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE');
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up test data before each test - more thorough cleanup
|
||||
await pool.query('DELETE FROM vehicles WHERE user_id = $1', ['test-user-123']);
|
||||
await pool.query('DELETE FROM vin_cache WHERE vin LIKE $1', ['1HGBH41JXMN%']);
|
||||
|
||||
// Clear Redis cache for the test user
|
||||
try {
|
||||
await cacheService.del('vehicles:user:test-user-123');
|
||||
} catch (error) {
|
||||
// Ignore cache cleanup errors in tests
|
||||
console.warn('Failed to clear Redis cache in test:', error);
|
||||
}
|
||||
});
|
||||
|
||||
describe('POST /api/vehicles', () => {
|
||||
it('should create a new vehicle', async () => {
|
||||
const vehicleData = {
|
||||
vin: '1HGBH41JXMN109186',
|
||||
nickname: 'My Test Car',
|
||||
color: 'Blue',
|
||||
odometerReading: 50000
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/vehicles')
|
||||
.send(vehicleData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
userId: 'test-user-123',
|
||||
vin: '1HGBH41JXMN109186',
|
||||
make: 'Honda',
|
||||
model: 'Civic',
|
||||
year: 2021,
|
||||
nickname: 'My Test Car',
|
||||
color: 'Blue',
|
||||
odometerReading: 50000,
|
||||
isActive: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid VIN', async () => {
|
||||
const vehicleData = {
|
||||
vin: 'INVALID',
|
||||
nickname: 'Test Car'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/vehicles')
|
||||
.send(vehicleData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toMatch(/VIN/);
|
||||
});
|
||||
|
||||
it('should reject duplicate VIN for same user', async () => {
|
||||
const vehicleData = {
|
||||
vin: '1HGBH41JXMN109186',
|
||||
nickname: 'First Car'
|
||||
};
|
||||
|
||||
// Create first vehicle
|
||||
await request(app)
|
||||
.post('/api/vehicles')
|
||||
.send(vehicleData)
|
||||
.expect(201);
|
||||
|
||||
// Try to create duplicate
|
||||
const response = await request(app)
|
||||
.post('/api/vehicles')
|
||||
.send({ ...vehicleData, nickname: 'Duplicate Car' })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBe('Vehicle with this VIN already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/vehicles', () => {
|
||||
it('should return user vehicles', async () => {
|
||||
// Create test vehicles
|
||||
await pool.query(`
|
||||
INSERT INTO vehicles (user_id, vin, make, model, year, nickname)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6),
|
||||
($7, $8, $9, $10, $11, $12)
|
||||
`, [
|
||||
'test-user-123', '1HGBH41JXMN109186', 'Honda', 'Civic', 2021, 'Car 1',
|
||||
'test-user-123', '1HGBH41JXMN109187', 'Toyota', 'Camry', 2020, 'Car 2'
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/vehicles')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body[0]).toMatchObject({
|
||||
userId: 'test-user-123',
|
||||
vin: expect.any(String),
|
||||
nickname: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for user with no vehicles', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/vehicles')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/vehicles/:id', () => {
|
||||
it('should return specific vehicle', async () => {
|
||||
// Create test vehicle
|
||||
const result = await pool.query(`
|
||||
INSERT INTO vehicles (user_id, vin, make, model, year, nickname)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
`, ['test-user-123', '1HGBH41JXMN109186', 'Honda', 'Civic', 2021, 'Test Car']);
|
||||
|
||||
const vehicleId = result.rows[0].id;
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/vehicles/${vehicleId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: vehicleId,
|
||||
userId: 'test-user-123',
|
||||
vin: '1HGBH41JXMN109186',
|
||||
make: 'Honda',
|
||||
nickname: 'Test Car'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent vehicle', async () => {
|
||||
const fakeId = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/vehicles/${fakeId}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.error).toBe('Vehicle not found');
|
||||
});
|
||||
|
||||
it('should return 400 for invalid UUID format', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/vehicles/invalid-id')
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/vehicles/:id', () => {
|
||||
it('should update vehicle', async () => {
|
||||
// Create test vehicle
|
||||
const result = await pool.query(`
|
||||
INSERT INTO vehicles (user_id, vin, nickname, color)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id
|
||||
`, ['test-user-123', '1HGBH41JXMN109186', 'Old Name', 'Blue']);
|
||||
|
||||
const vehicleId = result.rows[0].id;
|
||||
|
||||
const updateData = {
|
||||
nickname: 'Updated Name',
|
||||
color: 'Red',
|
||||
odometerReading: 75000
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/vehicles/${vehicleId}`)
|
||||
.send(updateData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: vehicleId,
|
||||
nickname: 'Updated Name',
|
||||
color: 'Red',
|
||||
odometerReading: 75000
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent vehicle', async () => {
|
||||
const fakeId = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/vehicles/${fakeId}`)
|
||||
.send({ nickname: 'Updated' })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.error).toBe('Vehicle not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/vehicles/:id', () => {
|
||||
it('should soft delete vehicle', async () => {
|
||||
// Create test vehicle
|
||||
const result = await pool.query(`
|
||||
INSERT INTO vehicles (user_id, vin, nickname)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`, ['test-user-123', '1HGBH41JXMN109186', 'Test Car']);
|
||||
|
||||
const vehicleId = result.rows[0].id;
|
||||
|
||||
await request(app)
|
||||
.delete(`/api/vehicles/${vehicleId}`)
|
||||
.expect(204);
|
||||
|
||||
// Verify vehicle is soft deleted
|
||||
const checkResult = await pool.query(
|
||||
'SELECT is_active, deleted_at FROM vehicles WHERE id = $1',
|
||||
[vehicleId]
|
||||
);
|
||||
|
||||
expect(checkResult.rows[0].is_active).toBe(false);
|
||||
expect(checkResult.rows[0].deleted_at).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent vehicle', async () => {
|
||||
const fakeId = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
const response = await request(app)
|
||||
.delete(`/api/vehicles/${fakeId}`)
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.error).toBe('Vehicle not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user