Calendar/test/utils/DateService.validation.test.ts

379 lines
12 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import { DateService } from '../../src/utils/DateService';
import { CalendarConfig } from '../../src/core/CalendarConfig';
describe('DateService - Validation', () => {
const config = new CalendarConfig();
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);
});
});
});