import { describe, it, expect } from 'vitest'; import { DateService } from '../../src/utils/DateService'; import { createTestConfig } from '../helpers/config-helpers'; describe('DateService - Validation', () => { const config = createTestConfig(); const dateService = new DateService(config); describe('isValid() - Basic Date Validation', () => { it('should validate normal dates', () => { const validDate = new Date(2024, 5, 15); expect(dateService.isValid(validDate)).toBe(true); }); it('should reject invalid date strings', () => { const invalidDate = new Date('not a date'); expect(dateService.isValid(invalidDate)).toBe(false); }); it('should reject NaN dates', () => { const nanDate = new Date(NaN); expect(dateService.isValid(nanDate)).toBe(false); }); it('should reject dates created from invalid input', () => { const invalidDate = new Date('2024-13-45'); // Invalid month and day expect(dateService.isValid(invalidDate)).toBe(false); }); it('should validate leap year dates', () => { const leapDay = new Date(2024, 1, 29); // Feb 29, 2024 expect(dateService.isValid(leapDay)).toBe(true); }); it('should detect invalid leap year dates', () => { const invalidLeapDay = new Date(2023, 1, 29); // Feb 29, 2023 (not leap year) // JavaScript auto-corrects this to March 1 expect(invalidLeapDay.getMonth()).toBe(2); // March expect(invalidLeapDay.getDate()).toBe(1); }); }); describe('isWithinBounds() - Date Range Validation', () => { it('should accept dates within bounds (1900-2100)', () => { const dates = [ new Date(1900, 0, 1), // Min bound new Date(1950, 6, 15), new Date(2000, 0, 1), new Date(2024, 5, 15), new Date(2100, 11, 31) // Max bound ]; dates.forEach(date => { expect(dateService.isWithinBounds(date)).toBe(true); }); }); it('should reject dates before 1900', () => { const tooEarly = [ new Date(1899, 11, 31), new Date(1800, 0, 1), new Date(1000, 6, 15) ]; tooEarly.forEach(date => { expect(dateService.isWithinBounds(date)).toBe(false); }); }); it('should reject dates after 2100', () => { const tooLate = [ new Date(2101, 0, 1), new Date(2200, 6, 15), new Date(3000, 0, 1) ]; tooLate.forEach(date => { expect(dateService.isWithinBounds(date)).toBe(false); }); }); it('should reject invalid dates', () => { const invalidDate = new Date('invalid'); expect(dateService.isWithinBounds(invalidDate)).toBe(false); }); it('should handle boundary dates exactly', () => { const minDate = new Date(1900, 0, 1, 0, 0, 0); const maxDate = new Date(2100, 11, 31, 23, 59, 59); expect(dateService.isWithinBounds(minDate)).toBe(true); expect(dateService.isWithinBounds(maxDate)).toBe(true); }); }); describe('isValidRange() - Date Range Validation', () => { it('should validate correct date ranges', () => { const start = new Date(2024, 0, 15); const end = new Date(2024, 0, 20); expect(dateService.isValidRange(start, end)).toBe(true); }); it('should accept equal start and end dates', () => { const date = new Date(2024, 0, 15, 10, 0); expect(dateService.isValidRange(date, date)).toBe(true); }); it('should reject reversed date ranges', () => { const start = new Date(2024, 0, 20); const end = new Date(2024, 0, 15); expect(dateService.isValidRange(start, end)).toBe(false); }); it('should reject ranges with invalid start date', () => { const invalidStart = new Date('invalid'); const validEnd = new Date(2024, 0, 20); expect(dateService.isValidRange(invalidStart, validEnd)).toBe(false); }); it('should reject ranges with invalid end date', () => { const validStart = new Date(2024, 0, 15); const invalidEnd = new Date('invalid'); expect(dateService.isValidRange(validStart, invalidEnd)).toBe(false); }); it('should validate ranges across year boundaries', () => { const start = new Date(2024, 11, 30); const end = new Date(2025, 0, 5); expect(dateService.isValidRange(start, end)).toBe(true); }); it('should validate multi-year ranges', () => { const start = new Date(2020, 0, 1); const end = new Date(2024, 11, 31); expect(dateService.isValidRange(start, end)).toBe(true); }); }); describe('validateDate() - Comprehensive Validation', () => { it('should validate normal dates without options', () => { const date = new Date(2024, 5, 15); const result = dateService.validateDate(date); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should reject invalid dates', () => { const invalidDate = new Date('invalid'); const result = dateService.validateDate(invalidDate); expect(result.valid).toBe(false); expect(result.error).toBe('Invalid date'); }); it('should reject out-of-bounds dates', () => { const tooEarly = new Date(1899, 0, 1); const result = dateService.validateDate(tooEarly); expect(result.valid).toBe(false); expect(result.error).toContain('out of bounds'); }); it('should validate future dates with requireFuture option', () => { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 10); const result = dateService.validateDate(futureDate, { requireFuture: true }); expect(result.valid).toBe(true); }); it('should reject past dates with requireFuture option', () => { const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 10); const result = dateService.validateDate(pastDate, { requireFuture: true }); expect(result.valid).toBe(false); expect(result.error).toContain('future'); }); it('should validate past dates with requirePast option', () => { const pastDate = new Date(); pastDate.setDate(pastDate.getDate() - 10); const result = dateService.validateDate(pastDate, { requirePast: true }); expect(result.valid).toBe(true); }); it('should reject future dates with requirePast option', () => { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + 10); const result = dateService.validateDate(futureDate, { requirePast: true }); expect(result.valid).toBe(false); expect(result.error).toContain('past'); }); it('should validate dates with minDate constraint', () => { const minDate = new Date(2024, 0, 1); const testDate = new Date(2024, 6, 15); const result = dateService.validateDate(testDate, { minDate }); expect(result.valid).toBe(true); }); it('should reject dates before minDate', () => { const minDate = new Date(2024, 6, 1); const testDate = new Date(2024, 5, 15); const result = dateService.validateDate(testDate, { minDate }); expect(result.valid).toBe(false); expect(result.error).toContain('after'); }); it('should validate dates with maxDate constraint', () => { const maxDate = new Date(2024, 11, 31); const testDate = new Date(2024, 6, 15); const result = dateService.validateDate(testDate, { maxDate }); expect(result.valid).toBe(true); }); it('should reject dates after maxDate', () => { const maxDate = new Date(2024, 6, 31); const testDate = new Date(2024, 7, 15); const result = dateService.validateDate(testDate, { maxDate }); expect(result.valid).toBe(false); expect(result.error).toContain('before'); }); it('should validate dates with both minDate and maxDate', () => { const minDate = new Date(2024, 0, 1); const maxDate = new Date(2024, 11, 31); const testDate = new Date(2024, 6, 15); const result = dateService.validateDate(testDate, { minDate, maxDate }); expect(result.valid).toBe(true); }); it('should reject dates outside min/max range', () => { const minDate = new Date(2024, 6, 1); const maxDate = new Date(2024, 6, 31); const testDate = new Date(2024, 7, 15); const result = dateService.validateDate(testDate, { minDate, maxDate }); expect(result.valid).toBe(false); }); }); describe('Invalid Date Scenarios', () => { it('should handle February 30 (auto-corrects to March)', () => { const invalidDate = new Date(2024, 1, 30); // Tries Feb 30, 2024 // JavaScript auto-corrects to March expect(invalidDate.getMonth()).toBe(2); // March expect(invalidDate.getDate()).toBe(1); }); it('should handle month overflow (month 13)', () => { const date = new Date(2024, 12, 1); // Month 13 = January next year expect(date.getFullYear()).toBe(2025); expect(date.getMonth()).toBe(0); // January }); it('should handle negative months', () => { const date = new Date(2024, -1, 1); // Month -1 = December previous year expect(date.getFullYear()).toBe(2023); expect(date.getMonth()).toBe(11); // December }); it('should handle day 0 (last day of previous month)', () => { const date = new Date(2024, 1, 0); // Day 0 of Feb = Last day of Jan expect(date.getMonth()).toBe(0); // January expect(date.getDate()).toBe(31); }); it('should handle negative days', () => { const date = new Date(2024, 1, -1); // Day -1 of Feb expect(date.getMonth()).toBe(0); // January expect(date.getDate()).toBe(30); }); }); describe('Timezone-aware Validation', () => { it('should validate UTC dates converted to local timezone', () => { const utcString = '2024-06-15T12:00:00Z'; const localDate = dateService.fromUTC(utcString); expect(dateService.isValid(localDate)).toBe(true); expect(dateService.isWithinBounds(localDate)).toBe(true); }); it('should maintain validation across timezone conversion', () => { const localDate = new Date(2024, 6, 15, 12, 0); const utcString = dateService.toUTC(localDate); const convertedBack = dateService.fromUTC(utcString); expect(dateService.isValid(convertedBack)).toBe(true); // Should be same day (accounting for timezone) const validation = dateService.validateDate(convertedBack); expect(validation.valid).toBe(true); }); it('should validate dates during DST transitions', () => { // Spring DST: March 31, 2024 in Copenhagen const dstDate = new Date(2024, 2, 31, 2, 30); // Non-existent hour // JavaScript handles this, should still be valid expect(dateService.isValid(dstDate)).toBe(true); }); }); describe('Edge Case Validation Combinations', () => { it('should reject invalid date even with lenient options', () => { const invalidDate = new Date('completely invalid'); const result = dateService.validateDate(invalidDate, { minDate: new Date(1900, 0, 1), maxDate: new Date(2100, 11, 31) }); expect(result.valid).toBe(false); expect(result.error).toBe('Invalid date'); }); it('should validate boundary dates with constraints', () => { const boundaryDate = new Date(1900, 0, 1); const result = dateService.validateDate(boundaryDate, { minDate: new Date(1900, 0, 1) }); expect(result.valid).toBe(true); }); it('should provide meaningful error messages', () => { const testCases = [ { date: new Date('invalid'), expectedError: 'Invalid date' }, { date: new Date(1800, 0, 1), expectedError: 'bounds' }, ]; testCases.forEach(({ date, expectedError }) => { const result = dateService.validateDate(date); expect(result.valid).toBe(false); expect(result.error).toContain(expectedError); }); }); it('should validate leap year boundaries correctly', () => { const leapYearEnd = new Date(2024, 1, 29); // Last day of Feb in leap year const nonLeapYearEnd = new Date(2023, 1, 28); // Last day of Feb in non-leap year expect(dateService.validateDate(leapYearEnd).valid).toBe(true); expect(dateService.validateDate(nonLeapYearEnd).valid).toBe(true); }); }); });