mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-11-07 02:45:15 +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) =>
|
const isUnixTimestamp = (value: unknown): boolean => {
|
||||||
intlFormat(parseISO(isoDate), {
|
if (typeof value !== "number" && typeof value !== "string") return false;
|
||||||
weekday: "long",
|
const num = Number(value);
|
||||||
year: "numeric",
|
if (!Number.isFinite(num)) return false;
|
||||||
month: "numeric",
|
// Check plausible Unix timestamp range: from 1970 to ~year 3000
|
||||||
day: "numeric",
|
// Support both seconds and milliseconds
|
||||||
hour: "numeric",
|
if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)
|
||||||
minute: "numeric",
|
if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)
|
||||||
second: "numeric",
|
return false;
|
||||||
hour12: true,
|
};
|
||||||
});
|
|
||||||
|
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 };
|
export { DateTimeFormat };
|
||||||
|
|||||||
@@ -30,13 +30,20 @@ const getLocale = (short = false) => {
|
|||||||
if (short) {
|
if (short) {
|
||||||
return loc.slice(0, 2);
|
return loc.slice(0, 2);
|
||||||
}
|
}
|
||||||
|
// finally, fallback
|
||||||
|
if (!loc) {
|
||||||
|
loc = "en";
|
||||||
|
}
|
||||||
return loc;
|
return loc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = createIntlCache();
|
const cache = createIntlCache();
|
||||||
|
|
||||||
const initialMessages = loadMessages(getLocale());
|
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 changeLocale = (locale: string): void => {
|
||||||
const messages = loadMessages(locale);
|
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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ if hash docker 2>/dev/null; then
|
|||||||
-e NODE_OPTIONS=--openssl-legacy-provider \
|
-e NODE_OPTIONS=--openssl-legacy-provider \
|
||||||
-v "$(pwd)/frontend:/app/frontend" \
|
-v "$(pwd)/frontend:/app/frontend" \
|
||||||
-w /app/frontend "${DOCKER_IMAGE}" \
|
-w /app/frontend "${DOCKER_IMAGE}" \
|
||||||
sh -c "yarn install && yarn lint && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
|
sh -c "yarn install && yarn lint && yarn vitest run && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
|
||||||
|
|
||||||
echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}"
|
echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user