← back to Jill Website
src/__tests__/routes/properties.test.ts
457 lines
import request from 'supertest';
import { Express } from 'express';
// Mock database before imports
jest.mock('../../config/database', () => ({
getPool: jest.fn(() => ({
query: jest.fn(),
connect: jest.fn()
})),
testConnection: jest.fn(),
closePool: jest.fn()
}));
// Mock models
jest.mock('../../models/Property');
import { PropertyModel } from '../../models/Property';
import { createTestApp } from './test-app';
describe('Properties API Endpoints', () => {
let app: Express;
let mockPropertyModel: jest.Mocked<PropertyModel>;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Setup mock property model
mockPropertyModel = {
create: jest.fn(),
findAll: jest.fn(),
findById: jest.fn(),
update: jest.fn(),
delete: jest.fn()
} as any;
// Create test app with mock model
app = createTestApp(undefined, mockPropertyModel);
});
describe('GET /api/properties', () => {
it('should return all properties with default pagination and 200 status', async () => {
const mockProperties = [
{
id: 1,
name: 'Casa del Mar',
description: 'Beautiful beachfront villa',
amenities: ['WiFi', 'Pool', 'Kitchen'],
images: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
airbnb_url: 'https://airbnb.com/property1',
capacity: 8,
bedrooms: 4,
bathrooms: 3,
size_sqm: 250,
price_per_night: 450,
created_at: new Date(),
updated_at: new Date()
},
{
id: 2,
name: 'Ocean View Suite',
description: 'Stunning ocean views',
amenities: ['WiFi', 'Air Conditioning'],
images: ['https://example.com/3.jpg'],
airbnb_url: 'https://airbnb.com/property2',
capacity: 4,
bedrooms: 2,
bathrooms: 2,
size_sqm: 150,
price_per_night: 250,
created_at: new Date(),
updated_at: new Date()
}
];
mockPropertyModel.findAll.mockResolvedValue(mockProperties);
const response = await request(app)
.get('/api/properties')
.expect(200);
expect(response.body).toEqual({
success: true,
data: expect.arrayContaining([
expect.objectContaining({ id: 1, name: 'Casa del Mar' }),
expect.objectContaining({ id: 2, name: 'Ocean View Suite' })
]),
count: mockProperties.length
});
expect(mockPropertyModel.findAll).toHaveBeenCalledWith(100, 0);
});
it('should return properties with custom pagination parameters', async () => {
const mockProperties = [
{
id: 3,
name: 'Jungle Retreat',
description: 'Secluded jungle property',
amenities: ['WiFi', 'Hot Tub'],
images: ['https://example.com/4.jpg'],
airbnb_url: 'https://airbnb.com/property3',
capacity: 6,
bedrooms: 3,
bathrooms: 2,
size_sqm: 200,
price_per_night: 350,
created_at: new Date(),
updated_at: new Date()
}
];
mockPropertyModel.findAll.mockResolvedValue(mockProperties);
const response = await request(app)
.get('/api/properties')
.query({ limit: 5, offset: 10 })
.expect(200);
expect(response.body.count).toBe(1);
expect(mockPropertyModel.findAll).toHaveBeenCalledWith(5, 10);
});
it('should return empty array when no properties exist', async () => {
mockPropertyModel.findAll.mockResolvedValue([]);
const response = await request(app)
.get('/api/properties')
.expect(200);
expect(response.body).toEqual({
success: true,
data: [],
count: 0
});
});
it('should return 500 for database errors', async () => {
mockPropertyModel.findAll.mockRejectedValue(new Error('Database connection failed'));
const response = await request(app)
.get('/api/properties')
.expect(500);
expect(response.body).toEqual({
success: false,
error: 'Internal server error',
message: 'Failed to fetch properties'
});
});
it('should handle large limit values', async () => {
mockPropertyModel.findAll.mockResolvedValue([]);
const response = await request(app)
.get('/api/properties')
.query({ limit: 1000, offset: 0 })
.expect(200);
expect(mockPropertyModel.findAll).toHaveBeenCalledWith(1000, 0);
expect(response.body.count).toBe(0);
});
});
describe('GET /api/properties/:id', () => {
it('should return property by valid ID and 200 status', async () => {
const mockProperty = {
id: 1,
name: 'Casa del Mar',
description: 'Beautiful beachfront villa',
amenities: ['WiFi', 'Pool', 'Kitchen'],
images: ['https://example.com/1.jpg'],
airbnb_url: 'https://airbnb.com/property1',
capacity: 8,
bedrooms: 4,
bathrooms: 3,
size_sqm: 250,
price_per_night: 450,
created_at: new Date(),
updated_at: new Date()
};
mockPropertyModel.findById.mockResolvedValue(mockProperty);
const response = await request(app)
.get('/api/properties/1')
.expect(200);
expect(response.body).toEqual({
success: true,
data: expect.objectContaining({
id: 1,
name: 'Casa del Mar',
description: 'Beautiful beachfront villa'
})
});
expect(mockPropertyModel.findById).toHaveBeenCalledWith(1);
});
it('should return 400 for invalid ID format (non-numeric)', async () => {
const response = await request(app)
.get('/api/properties/invalid-id')
.expect(400);
expect(response.body).toEqual({
success: false,
error: 'Invalid ID',
message: 'Property ID must be a number'
});
expect(mockPropertyModel.findById).not.toHaveBeenCalled();
});
it('should return 400 for invalid ID format (decimal)', async () => {
const response = await request(app)
.get('/api/properties/1.5')
.expect(400);
expect(response.body).toEqual({
success: false,
error: 'Invalid ID',
message: 'Property ID must be a number'
});
});
it('should return 404 for non-existent property', async () => {
mockPropertyModel.findById.mockResolvedValue(null);
const response = await request(app)
.get('/api/properties/999')
.expect(404);
expect(response.body).toEqual({
success: false,
error: 'Not found',
message: 'Property not found'
});
expect(mockPropertyModel.findById).toHaveBeenCalledWith(999);
});
it('should return 500 for database errors', async () => {
mockPropertyModel.findById.mockRejectedValue(new Error('Database query failed'));
const response = await request(app)
.get('/api/properties/1')
.expect(500);
expect(response.body).toEqual({
success: false,
error: 'Internal server error',
message: 'Failed to fetch property'
});
});
it('should handle negative ID gracefully', async () => {
mockPropertyModel.findById.mockResolvedValue(null);
const response = await request(app)
.get('/api/properties/-1')
.expect(404);
expect(mockPropertyModel.findById).toHaveBeenCalledWith(-1);
});
});
describe('Response Structure Validation', () => {
it('should have correct structure for successful GET all response', async () => {
const mockProperties = [
{
id: 1,
name: 'Test Property',
description: 'Test description',
amenities: ['WiFi'],
images: ['https://example.com/1.jpg'],
airbnb_url: 'https://airbnb.com/test',
capacity: 4,
bedrooms: 2,
bathrooms: 1,
size_sqm: 100,
price_per_night: 200,
created_at: new Date(),
updated_at: new Date()
}
];
mockPropertyModel.findAll.mockResolvedValue(mockProperties);
const response = await request(app)
.get('/api/properties');
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('data');
expect(response.body).toHaveProperty('count');
expect(Array.isArray(response.body.data)).toBe(true);
expect(typeof response.body.count).toBe('number');
});
it('should have correct structure for successful GET by ID response', async () => {
const mockProperty = {
id: 1,
name: 'Test Property',
description: 'Test description',
amenities: ['WiFi'],
images: ['https://example.com/1.jpg'],
airbnb_url: 'https://airbnb.com/test',
capacity: 4,
bedrooms: 2,
bathrooms: 1,
size_sqm: 100,
price_per_night: 200,
created_at: new Date(),
updated_at: new Date()
};
mockPropertyModel.findById.mockResolvedValue(mockProperty);
const response = await request(app)
.get('/api/properties/1');
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('data');
expect(response.body.data).toHaveProperty('id');
expect(response.body.data).toHaveProperty('name');
expect(response.body.data).toHaveProperty('description');
expect(response.body.data).toHaveProperty('amenities');
expect(response.body.data).toHaveProperty('images');
expect(response.body.data).toHaveProperty('airbnb_url');
});
it('should have correct structure for error responses', async () => {
mockPropertyModel.findAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/api/properties');
expect(response.body).toHaveProperty('success', false);
expect(response.body).toHaveProperty('error');
expect(response.body).toHaveProperty('message');
});
it('should verify property data types in response', async () => {
const mockProperty = {
id: 1,
name: 'Casa del Mar',
description: 'Beautiful villa',
amenities: ['WiFi', 'Pool'],
images: ['https://example.com/1.jpg'],
airbnb_url: 'https://airbnb.com/test',
capacity: 8,
bedrooms: 4,
bathrooms: 3,
size_sqm: 250,
price_per_night: 450,
created_at: new Date(),
updated_at: new Date()
};
mockPropertyModel.findById.mockResolvedValue(mockProperty);
const response = await request(app)
.get('/api/properties/1')
.expect(200);
const property = response.body.data;
expect(typeof property.id).toBe('number');
expect(typeof property.name).toBe('string');
expect(typeof property.description).toBe('string');
expect(Array.isArray(property.amenities)).toBe(true);
expect(Array.isArray(property.images)).toBe(true);
expect(typeof property.airbnb_url).toBe('string');
expect(typeof property.capacity).toBe('number');
expect(typeof property.bedrooms).toBe('number');
expect(typeof property.bathrooms).toBe('number');
expect(typeof property.size_sqm).toBe('number');
expect(typeof property.price_per_night).toBe('number');
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle extremely large pagination offset', async () => {
mockPropertyModel.findAll.mockResolvedValue([]);
const response = await request(app)
.get('/api/properties')
.query({ limit: 10, offset: 999999 })
.expect(200);
expect(mockPropertyModel.findAll).toHaveBeenCalledWith(10, 999999);
expect(response.body.data).toEqual([]);
});
it('should handle query timeout errors', async () => {
mockPropertyModel.findAll.mockRejectedValue(new Error('Query timeout exceeded'));
const response = await request(app)
.get('/api/properties')
.expect(500);
expect(response.body.message).toBe('Failed to fetch properties');
});
it('should handle connection pool exhaustion', async () => {
mockPropertyModel.findById.mockRejectedValue(new Error('Connection pool exhausted'));
const response = await request(app)
.get('/api/properties/1')
.expect(500);
expect(response.body.message).toBe('Failed to fetch property');
});
it('should handle query with string limit/offset', async () => {
mockPropertyModel.findAll.mockResolvedValue([]);
const response = await request(app)
.get('/api/properties')
.query({ limit: '20', offset: '10' })
.expect(200);
expect(mockPropertyModel.findAll).toHaveBeenCalledWith(20, 10);
});
it('should verify response status codes are correct', async () => {
// Test 200 for GET all
mockPropertyModel.findAll.mockResolvedValue([]);
await request(app).get('/api/properties').expect(200);
// Test 200 for GET by ID
mockPropertyModel.findById.mockResolvedValue({
id: 1,
name: 'Test',
description: 'Test',
amenities: ['WiFi'],
images: ['https://example.com/1.jpg'],
airbnb_url: 'https://airbnb.com/test',
created_at: new Date(),
updated_at: new Date()
});
await request(app).get('/api/properties/1').expect(200);
// Test 404 for not found
mockPropertyModel.findById.mockResolvedValue(null);
await request(app).get('/api/properties/999').expect(404);
// Test 400 for invalid ID
await request(app).get('/api/properties/invalid').expect(400);
// Test 500 for server error
mockPropertyModel.findAll.mockRejectedValue(new Error('Server error'));
await request(app).get('/api/properties').expect(500);
});
});
});