← back to Jill Website

src/__tests__/Property.test.ts

477 lines

import { PropertyModel, Property } from '../models/Property';
import { getTestPool } from './setup';

describe('PropertyModel Integration Tests', () => {
  let propertyModel: PropertyModel;

  beforeEach(() => {
    // Override the pool in PropertyModel to use test pool
    propertyModel = new PropertyModel();
    (propertyModel as any).pool = getTestPool();
  });

  describe('create', () => {
    it('should create a new property with valid data', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful beachfront property with stunning ocean views',
        amenities: ['Pool', 'WiFi', 'Air Conditioning', 'Kitchen'],
        images: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        capacity: 8,
        bedrooms: 4,
        bathrooms: 3,
        size_sqm: 200,
        price_per_night: 350,
      };

      const property = await propertyModel.create(propertyData);

      expect(property).toBeDefined();
      expect(property.id).toBeDefined();
      expect(property.name).toBe('Ocean View Villa');
      expect(property.description).toBe('Beautiful beachfront property with stunning ocean views');
      expect(property.amenities).toEqual(['Pool', 'WiFi', 'Air Conditioning', 'Kitchen']);
      expect(property.images).toEqual(['https://example.com/image1.jpg', 'https://example.com/image2.jpg']);
      expect(property.airbnb_url).toBe('https://www.airbnb.com/rooms/12345');
      expect(property.capacity).toBe(8);
      expect(property.bedrooms).toBe(4);
      expect(property.bathrooms).toBe(3);
      expect(property.size_sqm).toBe(200);
      expect(property.price_per_night).toBe(350);
      expect(property.created_at).toBeDefined();
      expect(property.updated_at).toBeDefined();
    });

    it('should create property with minimal required fields', async () => {
      const propertyData = {
        name: 'Simple Beach House',
        description: 'Cozy beach house',
        amenities: ['WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/54321',
      };

      const property = await propertyModel.create(propertyData);

      expect(property).toBeDefined();
      expect(property.id).toBeDefined();
      expect(property.name).toBe('Simple Beach House');
      expect(property.capacity).toBeNull();
      expect(property.bedrooms).toBeNull();
      expect(property.bathrooms).toBeNull();
    });

    it('should trim whitespace from text fields', async () => {
      const propertyData = {
        name: '  Ocean View Villa  ',
        description: '  Beautiful property  ',
        amenities: ['Pool', 'WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: '  https://www.airbnb.com/rooms/12345  ',
      };

      const property = await propertyModel.create(propertyData);
      expect(property.name).toBe('Ocean View Villa');
      expect(property.description).toBe('Beautiful property');
      expect(property.airbnb_url).toBe('https://www.airbnb.com/rooms/12345');
    });

    it('should reject property with missing name', async () => {
      const propertyData = {
        name: '',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Property name is required');
    });

    it('should reject property with missing description', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: '',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Property description is required');
    });

    it('should reject property with no amenities', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: [],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('At least one amenity is required');
    });

    it('should reject property with no images', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: [],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('At least one image is required');
    });

    it('should reject property with invalid image URL', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['not-a-valid-url'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('All image URLs must be valid');
    });

    it('should reject property with invalid Airbnb URL', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'not-a-valid-url',
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Valid Airbnb URL is required');
    });

    it('should reject property with negative capacity', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        capacity: 0,
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Capacity must be at least 1');
    });

    it('should reject property with negative bedrooms', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        bedrooms: -1,
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Bedrooms cannot be negative');
    });

    it('should reject property with negative price', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        price_per_night: -100,
      };

      await expect(propertyModel.create(propertyData)).rejects.toThrow('Price cannot be negative');
    });
  });

  describe('findById', () => {
    it('should retrieve property by ID', async () => {
      const propertyData = {
        name: 'Ocean View Villa',
        description: 'Beautiful property',
        amenities: ['Pool', 'WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        capacity: 8,
      };

      const created = await propertyModel.create(propertyData);
      const found = await propertyModel.findById(created.id!);

      expect(found).toBeDefined();
      expect(found!.id).toBe(created.id);
      expect(found!.name).toBe('Ocean View Villa');
      expect(found!.amenities).toEqual(['Pool', 'WiFi']);
      expect(found!.images).toEqual(['https://example.com/image.jpg']);
    });

    it('should return null for non-existent ID', async () => {
      const found = await propertyModel.findById(99999);
      expect(found).toBeNull();
    });

    it('should properly parse JSON fields', async () => {
      const propertyData = {
        name: 'Test Property',
        description: 'Test description',
        amenities: ['Amenity 1', 'Amenity 2', 'Amenity 3'],
        images: ['https://example.com/img1.jpg', 'https://example.com/img2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const created = await propertyModel.create(propertyData);
      const found = await propertyModel.findById(created.id!);

      expect(Array.isArray(found!.amenities)).toBe(true);
      expect(Array.isArray(found!.images)).toBe(true);
      expect(found!.amenities).toHaveLength(3);
      expect(found!.images).toHaveLength(2);
    });
  });

  describe('findAll', () => {
    it('should retrieve all properties', async () => {
      const property1 = {
        name: 'Property 1',
        description: 'First property',
        amenities: ['WiFi'],
        images: ['https://example.com/image1.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/11111',
      };

      const property2 = {
        name: 'Property 2',
        description: 'Second property',
        amenities: ['Pool'],
        images: ['https://example.com/image2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/22222',
      };

      await propertyModel.create(property1);
      await propertyModel.create(property2);

      const properties = await propertyModel.findAll();

      expect(properties).toHaveLength(2);
      expect(properties[0].name).toBe('Property 2'); // Most recent first
      expect(properties[1].name).toBe('Property 1');
    });

    it('should respect limit parameter', async () => {
      const property1 = {
        name: 'Property 1',
        description: 'First property',
        amenities: ['WiFi'],
        images: ['https://example.com/image1.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/11111',
      };

      const property2 = {
        name: 'Property 2',
        description: 'Second property',
        amenities: ['Pool'],
        images: ['https://example.com/image2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/22222',
      };

      await propertyModel.create(property1);
      await propertyModel.create(property2);

      const properties = await propertyModel.findAll(1);
      expect(properties).toHaveLength(1);
    });

    it('should return empty array when no properties exist', async () => {
      const properties = await propertyModel.findAll();
      expect(properties).toEqual([]);
    });

    it('should properly parse JSON fields for all properties', async () => {
      const propertyData = {
        name: 'Test Property',
        description: 'Test description',
        amenities: ['Amenity 1', 'Amenity 2'],
        images: ['https://example.com/img1.jpg', 'https://example.com/img2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await propertyModel.create(propertyData);
      const properties = await propertyModel.findAll();

      expect(properties).toHaveLength(1);
      expect(Array.isArray(properties[0].amenities)).toBe(true);
      expect(Array.isArray(properties[0].images)).toBe(true);
    });
  });

  describe('update', () => {
    it('should update property with valid data', async () => {
      const propertyData = {
        name: 'Original Name',
        description: 'Original description',
        amenities: ['WiFi'],
        images: ['https://example.com/original.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
        capacity: 4,
      };

      const created = await propertyModel.create(propertyData);

      const updateData = {
        name: 'Updated Name',
        description: 'Updated description',
        amenities: ['WiFi', 'Pool', 'Kitchen'],
        images: ['https://example.com/updated1.jpg', 'https://example.com/updated2.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/54321',
        capacity: 8,
        bedrooms: 4,
        bathrooms: 3,
        size_sqm: 250,
        price_per_night: 400,
      };

      const updated = await propertyModel.update(created.id!, updateData);

      expect(updated).toBeDefined();
      expect(updated!.id).toBe(created.id);
      expect(updated!.name).toBe('Updated Name');
      expect(updated!.description).toBe('Updated description');
      expect(updated!.amenities).toEqual(['WiFi', 'Pool', 'Kitchen']);
      expect(updated!.capacity).toBe(8);
      expect(updated!.bedrooms).toBe(4);
      expect(updated!.price_per_night).toBe(400);
    });

    it('should return null when updating non-existent property', async () => {
      const updateData = {
        name: 'Updated Name',
        description: 'Updated description',
        amenities: ['WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const updated = await propertyModel.update(99999, updateData);
      expect(updated).toBeNull();
    });

    it('should reject update with invalid data', async () => {
      const propertyData = {
        name: 'Original Name',
        description: 'Original description',
        amenities: ['WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const created = await propertyModel.create(propertyData);

      const invalidUpdate = {
        name: '',
        description: 'Updated description',
        amenities: ['WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      await expect(propertyModel.update(created.id!, invalidUpdate)).rejects.toThrow('Property name is required');
    });

    it('should properly handle array updates', async () => {
      const propertyData = {
        name: 'Test Property',
        description: 'Test description',
        amenities: ['WiFi'],
        images: ['https://example.com/image1.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const created = await propertyModel.create(propertyData);

      const updateData = {
        name: 'Test Property',
        description: 'Test description',
        amenities: ['WiFi', 'Pool', 'Kitchen', 'Air Conditioning'],
        images: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg', 'https://example.com/image3.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const updated = await propertyModel.update(created.id!, updateData);

      expect(updated!.amenities).toHaveLength(4);
      expect(updated!.images).toHaveLength(3);
    });
  });

  describe('delete', () => {
    it('should delete property by ID', async () => {
      const propertyData = {
        name: 'Test Property',
        description: 'Test description',
        amenities: ['WiFi'],
        images: ['https://example.com/image.jpg'],
        airbnb_url: 'https://www.airbnb.com/rooms/12345',
      };

      const created = await propertyModel.create(propertyData);
      const deleted = await propertyModel.delete(created.id!);

      expect(deleted).toBe(true);

      const found = await propertyModel.findById(created.id!);
      expect(found).toBeNull();
    });

    it('should return false when deleting non-existent property', async () => {
      const deleted = await propertyModel.delete(99999);
      expect(deleted).toBe(false);
    });
  });

  describe('data persistence', () => {
    it('should persist complex property data correctly', async () => {
      const propertyData = {
        name: 'Luxury Beach Villa',
        description: 'A stunning beachfront property with panoramic ocean views, modern amenities, and direct beach access',
        amenities: [
          'WiFi',
          'Pool',
          'Kitchen',
          'Air Conditioning',
          'Beach Access',
          'Parking',
          'Hot Tub',
          'BBQ Grill',
        ],
        images: [
          'https://example.com/villa-exterior.jpg',
          'https://example.com/villa-pool.jpg',
          'https://example.com/villa-bedroom.jpg',
          'https://example.com/villa-kitchen.jpg',
          'https://example.com/villa-view.jpg',
        ],
        airbnb_url: 'https://www.airbnb.com/rooms/luxury-beach-villa-12345',
        capacity: 12,
        bedrooms: 6,
        bathrooms: 5,
        size_sqm: 450.5,
        price_per_night: 850.99,
      };

      const created = await propertyModel.create(propertyData);
      const retrieved = await propertyModel.findById(created.id!);

      expect(retrieved).toBeDefined();
      expect(retrieved!.amenities).toHaveLength(8);
      expect(retrieved!.images).toHaveLength(5);
      expect(retrieved!.size_sqm).toBe(450.5);
      expect(retrieved!.price_per_night).toBe(850.99);
    });
  });
});