mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-07 10:55:14 +00:00
Safer handling of backend date formats
and add frontend testing
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { DateTimeFormat } from "src/locale";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
describe("DateFormatter", () => {
|
||||
// Keep a reference to the real Intl to restore later
|
||||
const RealIntl = global.Intl;
|
||||
const desiredTimeZone = "Europe/London";
|
||||
const desiredLocale = "en-GB";
|
||||
|
||||
beforeAll(() => {
|
||||
// Ensure Node-based libs using TZ behave deterministically
|
||||
try {
|
||||
process.env.TZ = desiredTimeZone;
|
||||
} catch {
|
||||
// ignore if not available
|
||||
}
|
||||
|
||||
// Mock Intl.DateTimeFormat so formatting is stable regardless of host
|
||||
const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
|
||||
constructor(
|
||||
_locales?: string | string[],
|
||||
options?: Intl.DateTimeFormatOptions,
|
||||
) {
|
||||
super(desiredLocale, {
|
||||
...options,
|
||||
timeZone: desiredTimeZone,
|
||||
});
|
||||
}
|
||||
} as unknown as typeof Intl.DateTimeFormat;
|
||||
|
||||
global.Intl = {
|
||||
...RealIntl,
|
||||
DateTimeFormat: MockedDateTimeFormat,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original Intl after tests
|
||||
global.Intl = RealIntl;
|
||||
});
|
||||
|
||||
it("format date from iso date", () => {
|
||||
const value = "2024-01-01T00:00:00.000Z";
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
|
||||
});
|
||||
|
||||
it("format date from unix timestamp number", () => {
|
||||
const value = 1762476112;
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
|
||||
});
|
||||
|
||||
it("format date from unix timestamp string", () => {
|
||||
const value = "1762476112";
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
|
||||
});
|
||||
|
||||
it("catch bad format from string", () => {
|
||||
const value = "this is not a good date";
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("this is not a good date");
|
||||
});
|
||||
|
||||
it("catch bad format from number", () => {
|
||||
const value = -100;
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("-100");
|
||||
});
|
||||
|
||||
it("catch bad format from number as string", () => {
|
||||
const value = "-100";
|
||||
const text = DateTimeFormat(value);
|
||||
expect(text).toBe("-100");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,36 @@
|
||||
import { intlFormat, parseISO } from "date-fns";
|
||||
import { fromUnixTime, intlFormat, parseISO } from "date-fns";
|
||||
|
||||
const DateTimeFormat = (isoDate: string) =>
|
||||
intlFormat(parseISO(isoDate), {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
const isUnixTimestamp = (value: unknown): boolean => {
|
||||
if (typeof value !== "number" && typeof value !== "string") return false;
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return false;
|
||||
// Check plausible Unix timestamp range: from 1970 to ~year 3000
|
||||
// Support both seconds and milliseconds
|
||||
if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)
|
||||
if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)
|
||||
return false;
|
||||
};
|
||||
|
||||
const DateTimeFormat = (value: string | number): string => {
|
||||
if (typeof value !== "number" && typeof value !== "string") return `${value}`;
|
||||
|
||||
try {
|
||||
const d = isUnixTimestamp(value)
|
||||
? fromUnixTime(+value)
|
||||
: parseISO(`${value}`);
|
||||
return intlFormat(d, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
} catch {
|
||||
return `${value}`;
|
||||
}
|
||||
};
|
||||
|
||||
export { DateTimeFormat };
|
||||
|
||||
@@ -30,13 +30,20 @@ const getLocale = (short = false) => {
|
||||
if (short) {
|
||||
return loc.slice(0, 2);
|
||||
}
|
||||
// finally, fallback
|
||||
if (!loc) {
|
||||
loc = "en";
|
||||
}
|
||||
return loc;
|
||||
};
|
||||
|
||||
const cache = createIntlCache();
|
||||
|
||||
const initialMessages = loadMessages(getLocale());
|
||||
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
|
||||
let intl = createIntl(
|
||||
{ locale: getLocale(), messages: initialMessages },
|
||||
cache,
|
||||
);
|
||||
|
||||
const changeLocale = (locale: string): void => {
|
||||
const messages = loadMessages(locale);
|
||||
@@ -76,4 +83,12 @@ const T = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
|
||||
export {
|
||||
localeOptions,
|
||||
getFlagCodeForLocale,
|
||||
getLocale,
|
||||
createIntl,
|
||||
changeLocale,
|
||||
intl,
|
||||
T,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user