← back to Jill Website

src/__tests__/routes/inquiries.test.ts

508 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/Inquiry');

import { InquiryModel } from '../../models/Inquiry';
import { createTestApp } from './test-app';

describe('Inquiries API Endpoints', () => {
  let app: Express;
  let mockInquiryModel: jest.Mocked<InquiryModel>;

  beforeEach(() => {
    // Reset mocks
    jest.clearAllMocks();

    // Setup mock inquiry model
    mockInquiryModel = {
      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(mockInquiryModel, undefined);
  });

  describe('POST /api/inquiries', () => {
    it('should create inquiry with valid data and return 201', async () => {
      const validInquiry = {
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: '2026-06-01',
        check_out_date: '2026-06-07',
        guest_count: 4,
        message: 'Looking forward to staying at your property!'
      };

      const createdInquiry = {
        id: 1,
        ...validInquiry,
        check_in_date: new Date('2026-06-01'),
        check_out_date: new Date('2026-06-07'),
        created_at: new Date(),
        updated_at: new Date()
      };

      mockInquiryModel.create.mockResolvedValue(createdInquiry);

      const response = await request(app)
        .post('/api/inquiries')
        .send(validInquiry)
        .expect(201);

      expect(response.body).toEqual({
        success: true,
        data: {
          id: createdInquiry.id,
          inquiry: expect.objectContaining({
            id: createdInquiry.id,
            name: validInquiry.name,
            email: validInquiry.email,
            phone: validInquiry.phone,
            guest_count: validInquiry.guest_count,
            message: validInquiry.message
          })
        },
        message: 'Inquiry created successfully'
      });

      expect(mockInquiryModel.create).toHaveBeenCalledWith(expect.objectContaining({
        name: validInquiry.name,
        email: validInquiry.email,
        phone: validInquiry.phone,
        guest_count: validInquiry.guest_count,
        message: validInquiry.message
      }));
    });

    it('should return 400 for missing required fields', async () => {
      const invalidInquiry = {
        name: 'John Doe',
        email: 'john@example.com'
        // Missing required fields
      };

      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: phone: Phone number is required, check_in_date: Check-in date is required')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send(invalidInquiry)
        .expect(400);

      expect(response.body).toEqual({
        success: false,
        error: 'Validation error',
        message: expect.stringContaining('Validation failed')
      });
    });

    it('should return 400 for invalid email format', async () => {
      const invalidInquiry = {
        name: 'John Doe',
        email: 'invalid-email',
        phone: '+1-555-1234',
        check_in_date: '2026-06-01',
        check_out_date: '2026-06-07',
        guest_count: 4,
        message: 'Test message'
      };

      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: email: Valid email is required')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send(invalidInquiry)
        .expect(400);

      expect(response.body).toEqual({
        success: false,
        error: 'Validation error',
        message: expect.stringContaining('Validation failed')
      });
    });

    it('should return 400 for invalid date range (checkout before checkin)', async () => {
      const invalidInquiry = {
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: '2026-06-07',
        check_out_date: '2026-06-01', // Before check-in
        guest_count: 4,
        message: 'Test message'
      };

      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: check_out_date: Check-out date must be after check-in date')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send(invalidInquiry)
        .expect(400);

      expect(response.body).toEqual({
        success: false,
        error: 'Validation error',
        message: expect.stringContaining('Validation failed')
      });
    });

    it('should return 400 for invalid guest count', async () => {
      const invalidInquiry = {
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: '2026-06-01',
        check_out_date: '2026-06-07',
        guest_count: 0, // Invalid
        message: 'Test message'
      };

      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: guest_count: Guest count must be at least 1')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send(invalidInquiry)
        .expect(400);

      expect(response.body).toEqual({
        success: false,
        error: 'Validation error',
        message: expect.stringContaining('Validation failed')
      });
    });

    it('should return 500 for database errors', async () => {
      const validInquiry = {
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: '2026-06-01',
        check_out_date: '2026-06-07',
        guest_count: 4,
        message: 'Test message'
      };

      mockInquiryModel.create.mockRejectedValue(new Error('Database connection failed'));

      const response = await request(app)
        .post('/api/inquiries')
        .send(validInquiry)
        .expect(500);

      expect(response.body).toEqual({
        success: false,
        error: 'Internal server error',
        message: 'Failed to create inquiry'
      });
    });

    it('should handle empty request body', async () => {
      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: name: Name is required, email: Valid email is required')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send({})
        .expect(400);

      expect(response.body.success).toBe(false);
      expect(response.body.error).toBe('Validation error');
    });
  });

  describe('GET /api/inquiries', () => {
    it('should return all inquiries with default pagination and 200 status', async () => {
      const mockInquiries = [
        {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com',
          phone: '+1-555-1234',
          check_in_date: new Date('2026-06-01'),
          check_out_date: new Date('2026-06-07'),
          guest_count: 4,
          message: 'Test message 1',
          created_at: new Date(),
          updated_at: new Date()
        },
        {
          id: 2,
          name: 'Jane Smith',
          email: 'jane@example.com',
          phone: '+1-555-5678',
          check_in_date: new Date('2026-07-01'),
          check_out_date: new Date('2026-07-07'),
          guest_count: 2,
          message: 'Test message 2',
          created_at: new Date(),
          updated_at: new Date()
        }
      ];

      mockInquiryModel.findAll.mockResolvedValue(mockInquiries);

      const response = await request(app)
        .get('/api/inquiries')
        .expect(200);

      expect(response.body).toEqual({
        success: true,
        data: expect.arrayContaining([
          expect.objectContaining({ id: 1, name: 'John Doe' }),
          expect.objectContaining({ id: 2, name: 'Jane Smith' })
        ]),
        count: mockInquiries.length
      });

      expect(mockInquiryModel.findAll).toHaveBeenCalledWith(100, 0);
    });

    it('should return inquiries with custom pagination parameters', async () => {
      const mockInquiries = [
        {
          id: 3,
          name: 'Bob Wilson',
          email: 'bob@example.com',
          phone: '+1-555-9999',
          check_in_date: new Date('2026-08-01'),
          check_out_date: new Date('2026-08-07'),
          guest_count: 6,
          message: 'Test message 3',
          created_at: new Date(),
          updated_at: new Date()
        }
      ];

      mockInquiryModel.findAll.mockResolvedValue(mockInquiries);

      const response = await request(app)
        .get('/api/inquiries')
        .query({ limit: 10, offset: 5 })
        .expect(200);

      expect(response.body.count).toBe(1);
      expect(mockInquiryModel.findAll).toHaveBeenCalledWith(10, 5);
    });

    it('should return empty array when no inquiries exist', async () => {
      mockInquiryModel.findAll.mockResolvedValue([]);

      const response = await request(app)
        .get('/api/inquiries')
        .expect(200);

      expect(response.body).toEqual({
        success: true,
        data: [],
        count: 0
      });
    });

    it('should return 500 for database errors', async () => {
      mockInquiryModel.findAll.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/api/inquiries')
        .expect(500);

      expect(response.body).toEqual({
        success: false,
        error: 'Internal server error',
        message: 'Failed to fetch inquiries'
      });
    });
  });

  describe('GET /api/inquiries/:id', () => {
    it('should return inquiry by valid ID with 200 status', async () => {
      const mockInquiry = {
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: new Date('2026-06-01'),
        check_out_date: new Date('2026-06-07'),
        guest_count: 4,
        message: 'Test message',
        created_at: new Date(),
        updated_at: new Date()
      };

      mockInquiryModel.findById.mockResolvedValue(mockInquiry);

      const response = await request(app)
        .get('/api/inquiries/1')
        .expect(200);

      expect(response.body).toEqual({
        success: true,
        data: expect.objectContaining({
          id: 1,
          name: 'John Doe',
          email: 'john@example.com'
        })
      });

      expect(mockInquiryModel.findById).toHaveBeenCalledWith(1);
    });

    it('should return 400 for invalid ID format', async () => {
      const response = await request(app)
        .get('/api/inquiries/invalid')
        .expect(400);

      expect(response.body).toEqual({
        success: false,
        error: 'Invalid ID',
        message: 'Inquiry ID must be a number'
      });

      expect(mockInquiryModel.findById).not.toHaveBeenCalled();
    });

    it('should return 404 for non-existent inquiry', async () => {
      mockInquiryModel.findById.mockResolvedValue(null);

      const response = await request(app)
        .get('/api/inquiries/999')
        .expect(404);

      expect(response.body).toEqual({
        success: false,
        error: 'Not found',
        message: 'Inquiry not found'
      });

      expect(mockInquiryModel.findById).toHaveBeenCalledWith(999);
    });

    it('should return 500 for database errors', async () => {
      mockInquiryModel.findById.mockRejectedValue(new Error('Database error'));

      const response = await request(app)
        .get('/api/inquiries/1')
        .expect(500);

      expect(response.body).toEqual({
        success: false,
        error: 'Internal server error',
        message: 'Failed to fetch inquiry'
      });
    });
  });

  describe('Response Structure Validation', () => {
    it('should have correct structure for successful POST response', async () => {
      const validInquiry = {
        name: 'John Doe',
        email: 'john@example.com',
        phone: '+1-555-1234',
        check_in_date: '2026-06-01',
        check_out_date: '2026-06-07',
        guest_count: 4,
        message: 'Test'
      };

      const createdInquiry = {
        id: 1,
        ...validInquiry,
        check_in_date: new Date('2026-06-01'),
        check_out_date: new Date('2026-06-07'),
        created_at: new Date(),
        updated_at: new Date()
      };

      mockInquiryModel.create.mockResolvedValue(createdInquiry);

      const response = await request(app)
        .post('/api/inquiries')
        .send(validInquiry);

      expect(response.body).toHaveProperty('success', true);
      expect(response.body).toHaveProperty('data');
      expect(response.body.data).toHaveProperty('id');
      expect(response.body.data).toHaveProperty('inquiry');
      expect(response.body).toHaveProperty('message');
    });

    it('should have correct structure for error responses', async () => {
      mockInquiryModel.create.mockRejectedValue(
        new Error('Validation failed: name: Name is required')
      );

      const response = await request(app)
        .post('/api/inquiries')
        .send({});

      expect(response.body).toHaveProperty('success', false);
      expect(response.body).toHaveProperty('error');
      expect(response.body).toHaveProperty('message');
    });

    it('should verify response status codes are correct', async () => {
      // Test 201 for successful creation
      mockInquiryModel.create.mockResolvedValue({
        id: 1,
        name: 'Test',
        email: 'test@example.com',
        phone: '+1-555-1234',
        check_in_date: new Date('2026-06-01'),
        check_out_date: new Date('2026-06-07'),
        guest_count: 4,
        message: 'Test',
        created_at: new Date(),
        updated_at: new Date()
      });

      await request(app)
        .post('/api/inquiries')
        .send({
          name: 'Test',
          email: 'test@example.com',
          phone: '+1-555-1234',
          check_in_date: '2026-06-01',
          check_out_date: '2026-06-07',
          guest_count: 4,
          message: 'Test'
        })
        .expect(201);

      // Test 200 for GET
      mockInquiryModel.findAll.mockResolvedValue([]);
      await request(app).get('/api/inquiries').expect(200);

      // Test 404 for not found
      mockInquiryModel.findById.mockResolvedValue(null);
      await request(app).get('/api/inquiries/999').expect(404);

      // Test 400 for invalid ID
      await request(app).get('/api/inquiries/invalid').expect(400);
    });
  });
});