Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions libs/accounts/email-renderer/src/renderer/email-helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,60 @@ describe('EmailHelpers', () => {
expect(resultDefault).toEqual('01/15/2024');
expect(resultEst).toEqual('01/14/2024');
});

it('does not leak locale between concurrent calls', async () => {
const date = new Date('2025-03-13T12:00:00Z');

const [enResult, gbResult] = await Promise.all([
Promise.resolve(constructLocalDateString(undefined, 'en', date)),
Promise.resolve(constructLocalDateString(undefined, 'en-GB', date)),
]);

expect(enResult).toEqual('03/13/2025');
expect(gbResult).toEqual('13/03/2025');
});

it('does not leak locale into subsequent calls', () => {
const date = new Date('2025-03-13T12:00:00Z');

constructLocalDateString(undefined, 'en-GB', date);
const enResult = constructLocalDateString(undefined, 'en', date);

expect(enResult).toEqual('03/13/2025');
});
});

describe('constructLocalTimeAndDateStrings - locale isolation', () => {
it('does not leak locale between concurrent calls', async () => {
const [enResult, esResult] = await Promise.all([
Promise.resolve(
constructLocalTimeAndDateStrings('America/Los_Angeles', 'en')
),
Promise.resolve(
constructLocalTimeAndDateStrings('America/Los_Angeles', 'es')
),
]);

const enDays = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
const esDays = [
'lunes',
'martes',
'miércoles',
'jueves',
'viernes',
'sábado',
'domingo',
];
expect(enDays).toContain(enResult.date.split(',')[0]);
expect(esDays).toContain(esResult.date.split(',')[0]);
});
});
});
20 changes: 6 additions & 14 deletions libs/accounts/email-renderer/src/renderer/email-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,11 @@ export const constructLocalTimeAndDateStrings = (
time: string;
timeZone: string;
} => {
moment.tz.setDefault(DEFAULT_TIMEZONE);

const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
moment.locale(locale);

let timeMoment = moment(date ? date : undefined);
if (timeZone) {
timeMoment = timeMoment.tz(timeZone);
}
const timeMoment = moment(date ? date : undefined)
.locale(locale)
.tz(timeZone || DEFAULT_TIMEZONE);

const formattedTime = timeMoment.format('LTS (z)');
const formattedDate = timeMoment.format('dddd, ll');
Expand All @@ -88,15 +84,11 @@ export const constructLocalDateString = (
date?: Date | number,
formatString = 'L'
): string => {
moment.tz.setDefault(DEFAULT_TIMEZONE);

const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
moment.locale(locale);

let time = moment(date);
if (timeZone) {
time = time.tz(timeZone);
}
const time = moment(date)
.locale(locale)
.tz(timeZone || DEFAULT_TIMEZONE);

return time.format(formatString);
};
21 changes: 6 additions & 15 deletions packages/fxa-auth-server/lib/senders/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const { ProductConfigurationManager } = require('@fxa/shared/cms');
const { Container } = require('typedi');

const DEFAULT_LOCALE = 'en';
const DEFAULT_TIMEZONE = 'Etc/UTC';
const UTM_PREFIX = 'fx-';

const X_SES_CONFIGURATION_SET = 'X-SES-CONFIGURATION-SET';
Expand Down Expand Up @@ -184,15 +183,11 @@ module.exports = function (log, config, bounces, statsd) {
}

function constructLocalTimeString(timeZone, locale) {
// if no timeZone is passed, use DEFAULT_TIMEZONE
moment.tz.setDefault(DEFAULT_TIMEZONE);
// if no locale is passed, use DEFAULT_LOCALE
locale = locale || DEFAULT_LOCALE;
moment.locale(locale);
let timeMoment = moment();
if (timeZone) {
timeMoment = timeMoment.tz(timeZone);
}
const timeMoment = moment()
.locale(locale)
.tz(timeZone || 'Etc/UTC');
// return a locale-specific time
// if date or time is passed, return it as the current date or time
const timeNow = timeMoment.format('LTS (z)');
Expand All @@ -206,15 +201,11 @@ module.exports = function (log, config, bounces, statsd) {
date,
formatString = 'L'
) {
// if no timeZone is passed, use DEFAULT_TIMEZONE
moment.tz.setDefault(DEFAULT_TIMEZONE);
// if no locale is passed, use DEFAULT_LOCALE
locale = locale || DEFAULT_LOCALE;
moment.locale(locale);
let time = moment(date);
if (timeZone) {
time = time.tz(timeZone);
}
const time = moment(date)
.locale(locale)
.tz(timeZone || 'Etc/UTC');
// return a locale-specific date
return time.format(formatString);
}
Expand Down
66 changes: 66 additions & 0 deletions packages/fxa-auth-server/test/local/senders/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6449,6 +6449,72 @@ describe('lib/senders/emails:', () => {
});
});

describe('constructLocalDateString - locale isolation', () => {
it('does not leak locale between concurrent calls', async () => {
const date = new Date('2025-03-13T12:00:00Z');

// Simulate concurrent calls with different locales
const [enResult, gbResult] = await Promise.all([
Promise.resolve(
mailer._constructLocalDateString(undefined, 'en', date)
),
Promise.resolve(
mailer._constructLocalDateString(undefined, 'en-GB', date)
),
]);

// en formats as MM/DD/YYYY, en-GB formats as DD/MM/YYYY
assert.equal(enResult, '03/13/2025');
assert.equal(gbResult, '13/03/2025');
});

it('does not leak locale into subsequent calls', () => {
const date = new Date('2025-03-13T12:00:00Z');

// Call with en-GB first
mailer._constructLocalDateString(undefined, 'en-GB', date);
// Then call with en
const enResult = mailer._constructLocalDateString(undefined, 'en', date);

assert.equal(enResult, '03/13/2025');
});
});

describe('constructLocalTimeString - locale isolation', () => {
it('does not leak locale between concurrent calls', async () => {
const [enResult, esResult] = await Promise.all([
Promise.resolve(
mailer._constructLocalTimeString('America/Los_Angeles', 'en')
),
Promise.resolve(
mailer._constructLocalTimeString('America/Los_Angeles', 'es')
),
]);

// en day names vs es day names
const enDays = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
const esDays = [
'lunes',
'martes',
'miércoles',
'jueves',
'viernes',
'sábado',
'domingo',
];
assert.include(enDays, enResult[1].split(',')[0]);
assert.include(esDays, esResult[1].split(',')[0]);
});
});

describe('constructLocalTimeString - returns date/time', () => {
// Moment expects a single locale identifier. This tests to ensure
// we account for this in _constructLocalTimeString
Expand Down
Loading