mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
208 lines
8.8 KiB
TypeScript
208 lines
8.8 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {
|
|
getErrorMessage,
|
|
getErrorMessageResult,
|
|
getErrorMessageUnsafe,
|
|
hasErrorLocale,
|
|
} from '@fluxer/errors/src/i18n/ErrorI18n';
|
|
import type {ErrorI18nKey} from '@fluxer/errors/src/i18n/ErrorI18nTypes.generated';
|
|
import {beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest';
|
|
|
|
describe('ErrorI18n', () => {
|
|
let consoleWarnSpy: MockInstance;
|
|
beforeEach(() => {
|
|
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
consoleWarnSpy.mockClear();
|
|
});
|
|
describe('constructor and initialization', () => {
|
|
it('loads the default TypeScript catalog', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'en-US');
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('initializes internal state correctly', () => {
|
|
expect(hasErrorLocale('en-US')).toBe(true);
|
|
});
|
|
it('handles missing default bundle gracefully', () => {
|
|
const message = getErrorMessageUnsafe('nonexistent.key', 'en-US', undefined, 'Fallback message');
|
|
expect(message).toBe('Fallback message');
|
|
});
|
|
});
|
|
describe('getMessage() - basic retrieval', () => {
|
|
it('returns message for valid key in default locale', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'en-US');
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('maps API error codes to localized messages', () => {
|
|
const message = getErrorMessageUnsafe('INVALID_FORM_BODY', 'en-US');
|
|
expect(message).toBe('Invalid form body.');
|
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
});
|
|
it('returns message for valid key in supported locale', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'fr');
|
|
expect(message).toBe('Tu es soumis à une limitation de débit.');
|
|
});
|
|
it('returns key when translation missing', () => {
|
|
const message = getErrorMessageUnsafe('nonexistent.key', 'en-US');
|
|
expect(message).toBe('nonexistent.key');
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
'Missing translation for error message: nonexistent.key (locale: en-US)',
|
|
);
|
|
});
|
|
it('returns fallbackMessage when provided and key missing', () => {
|
|
const message = getErrorMessageUnsafe('nonexistent.key', 'en-US', undefined, 'Custom fallback');
|
|
expect(message).toBe('Custom fallback');
|
|
});
|
|
});
|
|
describe('getMessage() - locale handling', () => {
|
|
it('normalizes en-GB locale to en-US', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'en-GB');
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('normalizes en-CA locale to en-US', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'en-CA');
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('falls back to en-US for unsupported locales', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'de-DE');
|
|
expect(message).toBe("You're being rate limited.");
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith('Unsupported locale, falling back to en-US: de-DE');
|
|
});
|
|
it('handles null locale by defaulting to en-US', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', null);
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('handles undefined locale by defaulting to en-US', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', undefined);
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('loads locale on-demand when first accessed', () => {
|
|
expect(hasErrorLocale('fr')).toBe(true);
|
|
const message = getErrorMessage('account.disabled', 'fr');
|
|
expect(message).toBe('Ce compte a été désactivé.');
|
|
});
|
|
});
|
|
describe('getMessage() - variable interpolation', () => {
|
|
it('interpolates simple {variable} placeholders', () => {
|
|
const message = getErrorMessage('channels_and_guilds.invalid_channel_id', 'en-US', {
|
|
channelId: '123456789',
|
|
});
|
|
expect(message).toBe('Invalid channel ID: 123456789.');
|
|
});
|
|
it('handles MessageFormat plural syntax', () => {
|
|
const message = getErrorMessage('rate_limits.username_changed_too_often', 'en-US', {
|
|
minutes: 1,
|
|
});
|
|
expect(message).toBe("You've changed your username too often recently. Please try again in 1 minute.");
|
|
});
|
|
it('handles MessageFormat plural syntax for multiple values', () => {
|
|
const message = getErrorMessage('rate_limits.username_changed_too_often', 'en-US', {
|
|
minutes: 5,
|
|
});
|
|
expect(message).toBe("You've changed your username too often recently. Please try again in 5 minutes.");
|
|
});
|
|
it('falls back to simple interpolation on MessageFormat failure', () => {
|
|
const message = getErrorMessage('channels_and_guilds.invalid_channel_id', 'en-US', {
|
|
channelId: 'test-channel',
|
|
});
|
|
expect(message).toBe('Invalid channel ID: test-channel.');
|
|
});
|
|
it('returns raw message when no variables provided', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'en-US');
|
|
expect(message).toBe("You're being rate limited.");
|
|
});
|
|
it('handles complex nested error keys', () => {
|
|
const message = getErrorMessage('roles.invalid_role_id', 'en-US', {roleId: '999'});
|
|
expect(message).toBe('Invalid role ID: 999.');
|
|
});
|
|
});
|
|
describe('getMessage() - edge cases', () => {
|
|
it('returns key when source message does not exist', () => {
|
|
const message = getErrorMessageUnsafe('completely.made.up.key', 'en-US');
|
|
expect(message).toBe('completely.made.up.key');
|
|
});
|
|
it('uses fallback when both key and fallbackMessage provided', () => {
|
|
const message = getErrorMessageUnsafe('missing.key', 'en-US', {}, 'Fallback used');
|
|
expect(message).toBe('Fallback used');
|
|
});
|
|
it('returns source message when locale translation missing but source exists', () => {
|
|
const message = getErrorMessage('rate_limits.rate_limited', 'xx-XX');
|
|
expect(message).toBe("You're being rate limited.");
|
|
expect(consoleWarnSpy).toHaveBeenCalledWith('Unsupported locale, falling back to en-US: xx-XX');
|
|
});
|
|
});
|
|
describe('getMessageResult()', () => {
|
|
it('returns error result for missing template', () => {
|
|
const result = getErrorMessageResult('missing.key' as ErrorI18nKey, 'en-US');
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.error.kind).toBe('missing-template');
|
|
}
|
|
});
|
|
});
|
|
describe('phone rejection messages', () => {
|
|
it.each([
|
|
[
|
|
'PHONE_COUNTRY_NOT_SUPPORTED',
|
|
"We don't send verification texts to this country. Use a mobile number from another country, or email [email protected] and a person will review your account.",
|
|
],
|
|
[
|
|
'PHONE_INBOUND_VERIFICATION_REQUIRED',
|
|
'This number is verified by texting us instead of us texting you. Start phone verification again to get the code and the number to text.',
|
|
],
|
|
[
|
|
'PHONE_LOOKUP_UNAVAILABLE',
|
|
'Our phone number check is down right now, so we stopped before sending your code. This is on us, not your number. Wait a few minutes and try the same number again.',
|
|
],
|
|
[
|
|
'PHONE_NUMBER_NOT_IN_SERVICE',
|
|
"Your carrier says this number isn't in service. Check the number and try again, or email [email protected] if it's correct.",
|
|
],
|
|
[
|
|
'PHONE_NUMBER_NOT_MOBILE',
|
|
"This isn't a mobile number, so it can't receive our text. Use a mobile number, or email [email protected] if you think that's wrong.",
|
|
],
|
|
[
|
|
'PHONE_VERIFICATION_NEEDS_REVIEW',
|
|
"We couldn't verify this number automatically. Email [email protected] and a person will review your account.",
|
|
],
|
|
])('resolves %s to its own message', (code, expected) => {
|
|
const message = getErrorMessageUnsafe(code, 'en-US');
|
|
expect(message).toBe(expected);
|
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
});
|
|
it.each([
|
|
'PHONE_COUNTRY_NOT_SUPPORTED',
|
|
'PHONE_NUMBER_NOT_IN_SERVICE',
|
|
'PHONE_NUMBER_NOT_MOBILE',
|
|
'PHONE_VERIFICATION_NEEDS_REVIEW',
|
|
])('routes %s to support', (code) => {
|
|
expect(getErrorMessageUnsafe(code, 'en-US')).toContain('[email protected]');
|
|
});
|
|
it('blames us for a lookup outage and invites the same number again', () => {
|
|
const message = getErrorMessage('phone.lookup_unavailable', 'en-US');
|
|
expect(message).toContain('This is on us, not your number.');
|
|
expect(message).toContain('try the same number again');
|
|
expect(message.toLowerCase()).not.toContain('invalid');
|
|
});
|
|
it('leaves the two fraud reasons indistinguishable', () => {
|
|
expect(getErrorMessageUnsafe('PHONE_VERIFICATION_NEEDS_REVIEW', 'en-US')).toBe(
|
|
getErrorMessage('phone.verification_needs_review', 'en-US'),
|
|
);
|
|
});
|
|
it('falls back to the source message when the locale has no catalog', () => {
|
|
expect(getErrorMessage('phone.lookup_unavailable', 'zz-ZZ')).toBe(
|
|
getErrorMessage('phone.lookup_unavailable', 'en-US'),
|
|
);
|
|
});
|
|
|
|
it('serves a real translation for a locale that has one', () => {
|
|
for (const locale of ['fr', 'de', 'ja', 'pt-BR']) {
|
|
expect(getErrorMessage('phone.lookup_unavailable', locale)).not.toBe(
|
|
getErrorMessage('phone.lookup_unavailable', 'en-US'),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|