← 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);
    });
  });
});