diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index 5b0fc02be..f421368a9 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -10,10 +10,10 @@ export function parseDateSafe(rawValue: string | null | undefined): Date | null try { // Handle formats like "YYYY-MM-DD HH:MM:SS" (SQLite) vs standard ISO-8601 const isoCompatible = raw.includes('T') ? raw : raw.replace(' ', 'T') - + // Check if the string already has timezone info (e.g. "Z" or "+HH:MM") const hasTimezone = /(?:Z|[+-]\d{2}:\d{2})$/.test(isoCompatible) - + // We try multiple candidate strings if timezone is missing const candidates = hasTimezone ? [isoCompatible, raw] @@ -21,12 +21,17 @@ export function parseDateSafe(rawValue: string | null | undefined): Date | null for (const candidate of candidates) { const d = new Date(candidate) - if (!Number.isNaN(d.getTime())) return d + const isValid = !Number.isNaN(d.getTime()) + + // Filter out invalid dates and unrealistic years (e.g., year 99999) + if (isValid && d.getFullYear() > 1900 && d.getFullYear() < 2100) { + return d + } } } catch (error) { console.error('Date parsing failed:', error, raw) } - + return null } @@ -34,26 +39,26 @@ export function parseDateSafe(rawValue: string | null | undefined): Date | null * Gets the preferred timezone from local storage or returns undefined to use system default. */ function getPreferredTimeZone(): string | undefined { - try { - const saved = localStorage.getItem('secuscan-config'); - if (saved) { - const config = JSON.parse(saved); - if (config.timezone && config.timezone !== 'auto') { - return config.timezone; - } - } - } catch (e) { - // Fallback to system default + try { + const saved = localStorage.getItem('secuscan-config') + if (saved) { + const config = JSON.parse(saved) + if (config.timezone && config.timezone !== 'auto') { + return config.timezone + } } - return undefined; + } catch (e) { + // Fallback to system default + } + return undefined } /** * Returns the current timezone being used (either preferred or system default). */ export function getCurrentTimeZone(): string { - const preferred = getPreferredTimeZone(); - if (preferred) return preferred; + const preferred = getPreferredTimeZone() + if (preferred) return preferred try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch (e) { @@ -66,16 +71,16 @@ export function getCurrentTimeZone(): string { */ export function getTimeZoneAbbreviation(): string { try { - const tz = getPreferredTimeZone(); - const formatter = new Intl.DateTimeFormat([], { + const tz = getPreferredTimeZone() + const formatter = new Intl.DateTimeFormat([], { timeZoneName: 'short', ...(tz ? { timeZone: tz } : {}) - }); - const parts = formatter.formatToParts(new Date()); - const tzPart = parts.find(part => part.type === 'timeZoneName'); - return tzPart ? tzPart.value : ''; + }) + const parts = formatter.formatToParts(new Date()) + const tzPart = parts.find(part => part.type === 'timeZoneName') + return tzPart ? tzPart.value : '' } catch (e) { - return ''; + return '' } } @@ -85,15 +90,15 @@ export function getTimeZoneAbbreviation(): string { export function formatBriefingDate(dateStr: string | null): string { const d = parseDateSafe(dateStr) if (!d) return '' - - const tz = getPreferredTimeZone(); - const options: Intl.DateTimeFormatOptions = tz ? { timeZone: tz } : {}; - + + const tz = getPreferredTimeZone() + const options: Intl.DateTimeFormatOptions = tz ? { timeZone: tz } : {} + const day = d.toLocaleDateString([], { ...options, day: '2-digit' }) const month = d.toLocaleDateString([], { ...options, month: 'short' }).toUpperCase() const year = d.toLocaleDateString([], { ...options, year: '2-digit' }) const time = d.toLocaleTimeString([], { ...options, hour: '2-digit', minute: '2-digit', hour12: false }) - + return `${day} ${month}, ${year}, ${time}` } @@ -103,15 +108,15 @@ export function formatBriefingDate(dateStr: string | null): string { export function formatTaskInit(dateStr: string): { date: string, time: string, tz: string } { const parsed = parseDateSafe(dateStr) if (!parsed) return { date: 'UNKNOWN DATE', time: 'UNKNOWN TIME', tz: '' } - - const tz = getPreferredTimeZone(); + + const tz = getPreferredTimeZone() const date = parsed.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric', ...(tz ? { timeZone: tz } : {}) }).toUpperCase() - + const time = parsed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', @@ -119,9 +124,9 @@ export function formatTaskInit(dateStr: string): { date: string, time: string, t hour12: false, ...(tz ? { timeZone: tz } : {}) }) - - const tzAbbr = getTimeZoneAbbreviation(); - + + const tzAbbr = getTimeZoneAbbreviation() + return { date, time, tz: tzAbbr } } @@ -131,8 +136,8 @@ export function formatTaskInit(dateStr: string): { date: string, time: string, t export function formatDateLong(dateStr: string | null): string { const d = parseDateSafe(dateStr) if (!d) return 'N/A' - - const tz = getPreferredTimeZone(); + + const tz = getPreferredTimeZone() const formatted = d.toLocaleString([], { day: '2-digit', month: 'short', @@ -143,37 +148,38 @@ export function formatDateLong(dateStr: string | null): string { hour12: false, ...(tz ? { timeZone: tz } : {}) }).toUpperCase() - - const tzAbbr = getTimeZoneAbbreviation(); - return tzAbbr ? `${formatted} ${tzAbbr}` : formatted; + + const tzAbbr = getTimeZoneAbbreviation() + return tzAbbr ? `${formatted} ${tzAbbr}` : formatted } /** * Shorthand for general toLocaleDateString without hardcoding. */ export function formatLocaleDate(dateStr: string | Date | null | undefined, options: Intl.DateTimeFormatOptions = {}): string { - const d = typeof dateStr === 'string' || dateStr === null || dateStr === undefined ? parseDateSafe(dateStr) : dateStr; - if (!d) return 'N/A'; - const tz = getPreferredTimeZone(); + const d = typeof dateStr === 'string' || dateStr === null || dateStr === undefined ? parseDateSafe(dateStr) : dateStr + if (!d) return 'N/A' + const tz = getPreferredTimeZone() return d.toLocaleDateString([], { ...(tz ? { timeZone: tz } : {}), ...options - }); + }) } /** * Shorthand for general toLocaleTimeString without hardcoding. */ export function formatLocaleTime(dateStr: string | Date | null | undefined, options: Intl.DateTimeFormatOptions = {}): string { - const d = typeof dateStr === 'string' || dateStr === null || dateStr === undefined ? parseDateSafe(dateStr) : dateStr; - if (!d) return 'N/A'; - const tz = getPreferredTimeZone(); + const d = typeof dateStr === 'string' || dateStr === null || dateStr === undefined ? parseDateSafe(dateStr) : dateStr + if (!d) return 'N/A' + const tz = getPreferredTimeZone() return d.toLocaleTimeString([], { ...(tz ? { timeZone: tz } : {}), hour12: false, ...options - }); + }) } + export type DateRange = 'all' | '24h' | '7d' | '30d' export function isWithinDateRange(dateStr: string, range: DateRange): boolean { diff --git a/frontend/testing/unit/utils/date.test.ts b/frontend/testing/unit/utils/date.test.ts index 7d878d329..75119f261 100644 --- a/frontend/testing/unit/utils/date.test.ts +++ b/frontend/testing/unit/utils/date.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeEach } from "vitest"; +import { describe, test, expect, beforeEach } from "vitest" import { parseDateSafe, @@ -7,109 +7,109 @@ import { formatLocaleTime, getCurrentTimeZone, formatTaskInit, -} from "../../../src/utils/date"; - - describe("date utilities", () => { - beforeEach(() => { - localStorage.clear(); - }); - - describe("parseDateSafe", () => { - test("parses ISO timestamp", () => { - const result = parseDateSafe("2026-05-12T10:30:00Z"); - expect(result).not.toBeNull(); - expect(result instanceof Date).toBe(true); - }); - - test("parses SQLite timestamp", () => { - const result = parseDateSafe("2026-05-12 10:30:00"); - expect(result).not.toBeNull(); - }); - - test("returns null for invalid input", () => { - expect(parseDateSafe("invalid-date")).toBeNull(); - }); - - test("returns null for empty string", () => { - expect(parseDateSafe("")).toBeNull(); - }); - - test("returns null for null input", () => { - expect(parseDateSafe(null)).toBeNull(); - }); - }); - - describe("formatDateLong", () => { - test("formats valid date", () => { - const result = formatDateLong("2026-05-12T10:30:00Z"); - expect(result).not.toBe("N/A"); - expect(result).toContain("2026"); - }); - - test("returns N/A for invalid input", () => { - expect(formatDateLong("bad-date")).toBe("N/A"); - }); - }); - - describe("formatLocaleDate", () => { - test("formats valid date", () => { - const result = formatLocaleDate("2026-05-12T10:30:00Z"); - expect(result).not.toBe("N/A"); - }); - - test("returns N/A for invalid input", () => { - expect(formatLocaleDate("bad-date")).toBe("N/A"); - }); - - test("returns N/A for null input", () => { - expect(formatLocaleDate(null)).toBe("N/A"); - }); - - test("returns N/A for undefined input", () => { - expect(formatLocaleDate(undefined)).toBe("N/A"); - }); - }); - - describe("formatLocaleTime", () => { - test("formats valid time", () => { - const result = formatLocaleTime("2026-05-12T10:30:00Z"); - expect(result).not.toBe("N/A"); - }); - - test("returns N/A for invalid input", () => { - expect(formatLocaleTime("bad-date")).toBe("N/A"); - }); - - test("returns N/A for null input", () => { - expect(formatLocaleTime(null)).toBe("N/A"); - }); - - test("returns N/A for undefined input", () => { - expect(formatLocaleTime(undefined)).toBe("N/A"); - }); - }); - - describe("timezone preference safety", () => { - test("does not crash without localStorage config", () => { - expect(() => formatLocaleDate("2026-05-12T10:30:00Z")).not.toThrow(); - }); - - test("uses fallback timezone safely", () => { - expect(getCurrentTimeZone()).toBeTruthy(); - }); - }); - - describe("formatTaskInit", () => { - test("returns UNKNOWN values for invalid date", () => { - const result = formatTaskInit("bad-date"); - expect(result.date).toBe("UNKNOWN DATE"); - expect(result.time).toBe("UNKNOWN TIME"); - }); - - test("formats valid task date", () => { - const result = formatTaskInit("2026-05-12T10:30:00Z"); - expect(result.date).not.toBe("UNKNOWN DATE"); - expect(result.time).not.toBe("UNKNOWN TIME"); - }); - }); - }); \ No newline at end of file +} from "../../../src/utils/date" + +describe("date utilities", () => { + beforeEach(() => { + localStorage.clear() + }) + + describe("parseDateSafe", () => { + test("parses ISO timestamp", () => { + const result = parseDateSafe("2026-05-12T10:30:00Z") + expect(result).not.toBeNull() + expect(result instanceof Date).toBe(true) + }) + + test("parses SQLite timestamp", () => { + const result = parseDateSafe("2026-05-12 10:30:00") + expect(result).not.toBeNull() + }) + + test("returns null for invalid input", () => { + expect(parseDateSafe("invalid-date")).toBeNull() + }) + + test("returns null for empty string", () => { + expect(parseDateSafe("")).toBeNull() + }) + + test("returns null for null input", () => { + expect(parseDateSafe(null)).toBeNull() + }) + }) + + describe("formatDateLong", () => { + test("formats valid date", () => { + const result = formatDateLong("2026-05-12T10:30:00Z") + expect(result).not.toBe("N/A") + expect(result).toContain("2026") + }) + + test("returns N/A for invalid input", () => { + expect(formatDateLong("bad-date")).toBe("N/A") + }) + }) + + describe("formatLocaleDate", () => { + test("formats valid date", () => { + const result = formatLocaleDate("2026-05-12T10:30:00Z") + expect(result).not.toBe("N/A") + }) + + test("returns N/A for invalid input", () => { + expect(formatLocaleDate("bad-date")).toBe("N/A") + }) + + test("returns N/A for null input", () => { + expect(formatLocaleDate(null)).toBe("N/A") + }) + + test("returns N/A for undefined input", () => { + expect(formatLocaleDate(undefined)).toBe("N/A") + }) + + describe("Issue #107 : Invalid Date Handling", () => { + test("returns N/A for unrealistic or non-standard dates", () => { + expect(formatLocaleDate("not-a-date")).toBe("N/A") + expect(formatLocaleDate("2026-13-45")).toBe("N/A") + expect(formatLocaleDate("99999")).toBe("N/A") + }) + }) + }) + + describe("formatLocaleTime", () => { + test("formats valid time", () => { + const result = formatLocaleTime("2026-05-12T10:30:00Z") + expect(result).not.toBe("N/A") + }) + + test("returns N/A for invalid input", () => { + expect(formatLocaleTime("bad-date")).toBe("N/A") + }) + }) + + describe("timezone preference safety", () => { + test("does not crash without localStorage config", () => { + expect(() => formatLocaleDate("2026-05-12T10:30:00Z")).not.toThrow() + }) + + test("uses fallback timezone safely", () => { + expect(getCurrentTimeZone()).toBeTruthy() + }) + }) + + describe("formatTaskInit", () => { + test("returns UNKNOWN values for invalid date", () => { + const result = formatTaskInit("bad-date") + expect(result.date).toBe("UNKNOWN DATE") + expect(result.time).toBe("UNKNOWN TIME") + }) + + test("formats valid task date", () => { + const result = formatTaskInit("2026-05-12T10:30:00Z") + expect(result.date).not.toBe("UNKNOWN DATE") + expect(result.time).not.toBe("UNKNOWN TIME") + }) + }) +})