chore(repo): remove dead code and useless tests (#3375)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 21:43:29 +02:00
1 parent 7e177a1c90
commit 1cd62eb661
1391 files changed
+783 -120415

No files matched your search

@@ -1,394 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {EmailI18nService, type IEmailI18nService} from '@pkgs/email/src/EmailI18nService';
import type {EmailConfig, EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
import {EmailService} from '@pkgs/email/src/EmailService';
import {TestEmailService} from '@pkgs/email/src/TestEmailService';
import {describe, expect, it} from 'vitest';
const CONFIG: EmailConfig = {
enabled: true,
fromEmail: '[email protected]',
fromName: 'Fluxer',
appBaseUrl: 'https://example.com',
termsUrl: 'https://example.com/terms',
guidelinesUrl: 'https://example.com/guidelines',
appealsEmail: '[email protected]',
safetyEmail: '[email protected]',
supportEmail: '[email protected]',
productName: 'Fluxer',
};
async function sendWith(config: EmailConfig): Promise<EmailMessage> {
const sent: Array<EmailMessage> = [];
const provider: IEmailProvider = {
sendEmail: async (message) => {
sent.push(message);
return true;
},
};
const service = new EmailService(config, new EmailI18nService(), provider);
await expect(service.sendEmailChangeNew('[email protected]', 'testuser', '123456', 'en-US')).resolves.toBe(true);
expect(sent).toHaveLength(1);
return sent[0];
}
describe('EmailService reply-to', () => {
it('sets the configured reply-to address on every message', async () => {
const message = await sendWith({...CONFIG, replyTo: '[email protected]'});
expect(message.replyTo).toBe('[email protected]');
expect(message.from).toEqual({email: '[email protected]', name: 'Fluxer'});
});
it.each([undefined, null, ''])('omits the reply-to address when it is %j', async (replyTo) => {
const message = await sendWith({...CONFIG, replyTo});
expect(message).not.toHaveProperty('replyTo');
});
});
function createCapturingServiceFor(config: EmailConfig): {service: EmailService; sent: Array<EmailMessage>} {
const sent: Array<EmailMessage> = [];
const provider: IEmailProvider = {
sendEmail: async (message) => {
sent.push(message);
return true;
},
};
return {service: new EmailService(config, new EmailI18nService(), provider), sent};
}
function createCapturingService(): {service: EmailService; sent: Array<EmailMessage>} {
return createCapturingServiceFor(CONFIG);
}
describe('EmailService report notices', () => {
it('sends a receipt with the report id and target kind', async () => {
const {service, sent} = createCapturingService();
await expect(service.sendReportReceivedEmail('[email protected]', '1234567890', 'guild', 'en-US')).resolves.toBe(
true,
);
expect(sent).toHaveLength(1);
expect(sent[0].to).toBe('[email protected]');
expect(sent[0].subject).toBe('We received your Fluxer report');
expect(sent[0].text).toContain('report about a community on Fluxer.');
expect(sent[0].text).toContain('Report ID: 1234567890');
});
it('sends the receipt in the requested locale', async () => {
const {service, sent} = createCapturingService();
await service.sendReportReceivedEmail('[email protected]', '1234567890', 'message', 'de');
expect(sent[0].subject).toBe('Wir haben deine Fluxer-Meldung erhalten');
expect(sent[0].text).toContain('Meldungs-ID: 1234567890');
});
it('sends a DSA decision notice with the public comment', async () => {
const {service, sent} = createCapturingService();
await expect(
service.sendDsaReportResolvedEmail('[email protected]', '1234567890', 'We removed the content.', 'en-US'),
).resolves.toBe(true);
expect(sent).toHaveLength(1);
expect(sent[0].to).toBe('[email protected]');
expect(sent[0].subject).toBe('We made a decision on your Fluxer report');
expect(sent[0].text).toContain('(ID: 1234567890)');
expect(sent[0].text).toContain('Response from the Safety Team:\nWe removed the content.');
expect(sent[0].text).toContain('Email [email protected] from this email address');
});
it('sends a generic DSA decision notice without a public comment', async () => {
const {service, sent} = createCapturingService();
await service.sendDsaReportResolvedEmail('[email protected]', '1234567890', '', 'en-US');
expect(sent[0].text).not.toContain('Response from the Safety Team');
expect(sent[0].text).not.toContain('\n\n\n');
expect(sent[0].text).toContain('Email [email protected] from this email address');
});
});
describe('TestEmailService report notices', () => {
it('records the receipt and the DSA decision notice', async () => {
const service = new TestEmailService();
await service.sendReportReceivedEmail('[email protected]', '1234567890', 'user', 'en-US');
await service.sendDsaReportResolvedEmail('[email protected]', '1234567890', '', 'en-US');
expect(service.listSentEmails().map(({to, type, metadata}) => ({to, type, metadata}))).toEqual([
{to: '[email protected]', type: 'report_received', metadata: {report_id: '1234567890', target_kind: 'user'}},
{
to: '[email protected]',
type: 'dsa_report_resolved',
metadata: {report_id: '1234567890', public_comment: ''},
},
]);
});
});
describe('EmailService enforcement notices', () => {
function capture(config: EmailConfig): {service: EmailService; sent: Array<EmailMessage>} {
const sent: Array<EmailMessage> = [];
const provider: IEmailProvider = {
sendEmail: async (message) => {
sent.push(message);
return true;
},
};
return {service: new EmailService(config, new EmailI18nService(), provider), sent};
}
async function sendBoth(config: EmailConfig, reason: string | null = 'Spam'): Promise<Array<string>> {
const {service, sent} = capture(config);
const until = new Date('2026-10-01T23:30:00Z');
await service.sendAccountTempBannedEmail('[email protected]', 'testuser', reason, 24, until, 'en-US');
await service.sendAccountScheduledForDeletionEmail('[email protected]', 'testuser', reason, until, 'en-US');
expect(sent).toHaveLength(2);
return sent.map((message) => message.text);
}
it('links the configured terms and guidelines', async () => {
for (const text of await sendBoth(CONFIG)) {
expect(text).toContain(
'Please review:\n- Terms of Service: https://example.com/terms\n- Community Guidelines: https://example.com/guidelines\n\n',
);
}
});
it('leaves out the review list when no policy page is configured', async () => {
for (const text of await sendBoth({...CONFIG, termsUrl: null, guidelinesUrl: null})) {
expect(text).not.toContain('Please review:');
expect(text).not.toContain('https://');
expect(text).not.toContain('\n\n\n');
expect(text).toContain('[email protected]');
}
});
it('lists only the configured page', async () => {
for (const text of await sendBoth({...CONFIG, termsUrl: null, guidelinesUrl: 'https://rules.example.org'})) {
expect(text).toContain('Please review:\n- Community Guidelines: https://rules.example.org\n\n');
expect(text).not.toContain('Terms of Service:');
}
for (const text of await sendBoth({...CONFIG, termsUrl: 'https://tos.example.org', guidelinesUrl: null})) {
expect(text).toContain('Please review:\n- Terms of Service: https://tos.example.org\n\n');
expect(text).not.toContain('Community Guidelines:');
}
});
it('states the reason as its own paragraph', async () => {
const [suspended, deletion] = await sendBoth(CONFIG);
expect(suspended).toContain(' UTC\n\nReason: Spam\n\nDuring this time,');
expect(deletion).toContain(' UTC\n\nReason: Spam\n\nThis is a serious enforcement action.');
for (const text of [suspended, deletion]) {
expect(text).not.toContain('\n\n\n');
}
});
it.each([null, '', ' '])('leaves no gap when the reason is %j', async (reason) => {
const withReason = await sendBoth(CONFIG);
const [suspended, deletion] = await sendBoth(CONFIG, reason);
expect(suspended).toContain(' UTC\n\nDuring this time,');
expect(deletion).toContain(' UTC\n\nThis is a serious enforcement action.');
for (const [index, text] of [suspended, deletion].entries()) {
expect(text).not.toContain('Reason:');
expect(text).not.toContain('\n\n\n');
expect(text.split('\n\n')).toHaveLength(withReason[index].split('\n\n').length - 1);
}
});
});
describe('EmailService contact addresses', () => {
const SELF_HOSTED: EmailConfig = {...CONFIG, appealsEmail: null, safetyEmail: null, supportEmail: null};
const DATE = new Date('2026-10-01T23:30:00Z');
async function sendAll(config: EmailConfig, locale: string): Promise<Record<string, string>> {
const sent: Array<EmailMessage> = [];
const provider: IEmailProvider = {
sendEmail: async (message) => {
sent.push(message);
return true;
},
};
const service = new EmailService(config, new EmailI18nService(), provider);
const to = '[email protected]';
await service.sendAccountTempBannedEmail(to, 'testuser', 'Spam', 24, DATE, locale);
await service.sendAccountScheduledForDeletionEmail(to, 'testuser', 'Spam', DATE, locale);
await service.sendScheduledDeletionNotification(to, 'testuser', DATE, 'Spam', locale);
await service.sendDsaReportResolvedEmail(to, '1234567890', 'We removed the content.', locale);
await service.sendReportResolvedEmail(to, 'testuser', '1234567890', 'We removed the content.', locale);
await service.sendAccountDeletionCancelledEmail(to, 'testuser', locale);
await service.sendAccountDeletionRequestedEmail(to, 'testuser', null, DATE, locale);
await service.sendAccountDeletionInactivityEmail(to, 'testuser', null, DATE, locale);
expect(sent).toHaveLength(8);
const [tempBan, deletion, deletionNotice, dsaResolved, resolved, cancelled, requested, inactivity] = sent.map(
(message) => message.text,
);
return {tempBan, deletion, deletionNotice, dsaResolved, resolved, cancelled, requested, inactivity};
}
it('names the hosted mailboxes in en-US', async () => {
const hosted = await sendAll(CONFIG, 'en-US');
expect(hosted.tempBan).toContain(
'Email [email protected] from this email address and clearly explain why you believe the decision was incorrect.',
);
expect(hosted.deletion).toContain(
'you have 60 days to submit an appeal. Email [email protected] from this email address.\n',
);
expect(hosted.deletionNotice).toContain(
'you can submit an appeal. Email [email protected] from this email address.\n',
);
expect(hosted.dsaResolved).toContain('Email [email protected] from this email address, include your report ID,');
expect(hosted.resolved).toContain('please contact [email protected].\n');
expect(hosted.cancelled).toContain('If you have any questions, contact [email protected].\n');
expect(hosted.requested).toContain('contact [email protected] from this email address before that date.\n');
expect(hosted.inactivity).toContain('contact [email protected] from this email address before that date.\n');
});
it('points at the instance administrators in en-US when no mailbox is configured', async () => {
const neutral = await sendAll(SELF_HOSTED, 'en-US');
expect(neutral.tempBan).toContain(
'you can submit an appeal. Contact the administrators of this instance and clearly explain why you believe the decision was incorrect.',
);
expect(neutral.deletion).toContain(
'you have 60 days to submit an appeal. Contact the administrators of this instance.\n',
);
expect(neutral.deletionNotice).toContain(
'you can submit an appeal. Contact the administrators of this instance.\n',
);
expect(neutral.dsaResolved).toContain(
'Contact the administrators of this instance, include your report ID, and explain why you think the decision is wrong.',
);
expect(neutral.resolved).toContain(
'If you have any questions or concerns about this outcome, please contact the administrators of this instance.\n',
);
expect(neutral.cancelled).toContain('If you have any questions, contact the administrators of this instance.\n');
expect(neutral.requested).toContain(
"Your account is locked until then. If you didn't request this, or you want to keep your account, contact the administrators of this instance before that date.\n",
);
expect(neutral.inactivity).toContain(
'If you want to keep your account, contact the administrators of this instance before that date.\n',
);
});
it.each([
['de', 'die Administratoren dieser Instanz', 'Sende eine E-Mail von dieser E-Mail-Adresse an [email protected]'],
['ja', 'このインスタンスの管理者', 'このメールアドレスから[email protected]までメールを送信'],
])('renders both variants in %s', async (locale, administrators, hostedAppeal) => {
const hosted = await sendAll(CONFIG, locale);
const neutral = await sendAll(SELF_HOSTED, locale);
expect(hosted.tempBan).toContain(hostedAppeal);
for (const [name, text] of Object.entries(hosted)) {
expect(text, name).toMatch(/(appeals|safety)@fluxer\.com/);
expect(text, name).not.toContain(administrators);
}
for (const [name, text] of Object.entries(neutral)) {
expect(text, name).toContain(administrators);
expect(text, name).not.toContain('@');
expect(text, name).not.toMatch(/\bnull\b/);
expect(text, name).not.toContain('{');
expect(text, name).not.toContain('\n\n\n');
expect(text.split('\n'), name).toHaveLength(hosted[name].split('\n').length);
}
});
});
describe('EmailService support contact', () => {
const SELF_HOSTED: EmailConfig = {...CONFIG, appealsEmail: null, safetyEmail: null, supportEmail: null};
const DATE = new Date('2026-10-01T23:30:00Z');
async function sendSupportEmails(config: EmailConfig, locale: string): Promise<Array<string>> {
const {service, sent} = createCapturingServiceFor(config);
const to = '[email protected]';
await service.sendGiftChargebackNotification(to, 'testuser', locale);
await service.sendHarvestCompletedEmail(to, 'testuser', 'https://example.com/d', 10, 1024, DATE, locale);
await service.sendInactivityWarningEmail(to, 'testuser', DATE, DATE, locale);
expect(sent).toHaveLength(3);
return sent.map((message) => message.text);
}
it('keeps the support team sentences on the hosted instance', async () => {
const [gift, harvest, inactivity] = await sendSupportEmails(CONFIG, 'en-US');
expect(gift).toContain(
'\n\nIf you think this is a mistake, please contact our support team and include any details you have about the gift code and when you redeemed it.\n\n',
);
expect(harvest).toContain(
"\n\nIf you didn't request this export, please change your password immediately and contact our support team.\n\n",
);
expect(inactivity).toContain("\n\nIf you've used Fluxer recently, please contact our support team right away.\n\n");
});
it('points at the instance administrators when the instance has no support mailbox', async () => {
const [gift, harvest, inactivity] = await sendSupportEmails({...SELF_HOSTED, productName: 'Example Chat'}, 'en-US');
expect(gift).toContain(
'\n\nIf you think this is a mistake, please contact the administrators of this instance and include any details you have about the gift code and when you redeemed it.\n\n',
);
expect(harvest).toContain(
"\n\nIf you didn't request this export, please change your password immediately and contact the administrators of this instance.\n\n",
);
expect(inactivity).toContain(
"\n\nIf you've used Example Chat recently, please contact the administrators of this instance right away.\n\n",
);
for (const text of [gift, harvest, inactivity]) {
expect(text).not.toMatch(/support/i);
}
});
it.each([
['de', 'unser Support-Team', 'die Administratoren dieser Instanz'],
['ja', 'サポートチーム', 'このインスタンスの管理者'],
])('renders both variants in %s', async (locale, supportTeam, administrators) => {
const hosted = await sendSupportEmails(CONFIG, locale);
const neutral = await sendSupportEmails(SELF_HOSTED, locale);
for (const [index, text] of hosted.entries()) {
expect(text).toContain(supportTeam);
expect(text).not.toContain(administrators);
expect(neutral[index]).toContain(administrators);
expect(neutral[index]).not.toContain(supportTeam);
expect(neutral[index].split('\n')).toHaveLength(text.split('\n').length);
}
});
});
describe('EmailService product name', () => {
async function sendPasswordReset(config: EmailConfig, locale: string): Promise<EmailMessage> {
const {service, sent} = createCapturingServiceFor(config);
await expect(service.sendPasswordResetEmail('[email protected]', 'testuser', 'token', locale)).resolves.toBe(true);
expect(sent).toHaveLength(1);
return sent[0];
}
it('names Fluxer on the hosted instance', async () => {
const message = await sendPasswordReset(CONFIG, 'en-US');
expect(message.subject).toBe('Reset your Fluxer password');
expect(message.text).toContain('Fluxer');
});
it.each(['en-US', 'de', 'ja'])('names the configured instance in the %s subject and body', async (locale) => {
const message = await sendPasswordReset({...CONFIG, productName: 'Example Chat'}, locale);
expect(message.subject).toContain('Example Chat');
expect(message.text).toContain('Example Chat');
expect(`${message.subject}\n${message.text}`).not.toContain('Fluxer');
});
it('uses the name of the config it was built with, so a renamed instance sends the new name', async () => {
const before = await sendPasswordReset({...CONFIG, productName: 'Example Chat'}, 'en-US');
const after = await sendPasswordReset({...CONFIG, productName: 'Renamed Chat'}, 'en-US');
expect(before.subject).toBe('Reset your Example Chat password');
expect(after.subject).toBe('Reset your Renamed Chat password');
expect(after.text).not.toContain('Example Chat');
});
it('passes the name to every template the service sends', async () => {
const names: Array<string> = [];
const i18n = new EmailI18nService();
const recording: IEmailI18nService = {
getTemplate: (key, locale, variables, productName) => {
names.push(productName);
return i18n.getTemplate(key, locale, variables, productName);
},
};
const service = new EmailService({...CONFIG, productName: 'Example Chat'}, recording, {
sendEmail: async () => true,
});
const date = new Date('2026-10-01T23:30:00Z');
const to = '[email protected]';
await service.sendEmailVerification(to, 'testuser', 'token', 'en-US');
await service.sendAccountTempBannedEmail(to, 'testuser', 'Spam', 24, date, 'en-US');
await service.sendReportResolvedEmail(to, 'testuser', '1', 'Thanks', 'en-US');
await service.sendSelfDeletionScheduledEmail(to, 'testuser', date, 'en-US');
expect(names).toEqual(['Example Chat', 'Example Chat', 'Example Chat', 'Example Chat']);
});
});
@@ -1,44 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
import {beforeEach, describe, expect, it, vi} from 'vitest';
const {sendMail} = vi.hoisted(() => ({sendMail: vi.fn()}));
vi.mock('nodemailer', () => ({
default: {createTransport: () => ({sendMail, verify: vi.fn()})},
}));
const MESSAGE = {
to: '[email protected]',
from: {email: '[email protected]', name: 'Fluxer'},
subject: 'Subject',
text: 'Body',
};
function createProvider(): SmtpEmailProvider {
return new SmtpEmailProvider({host: 'smtp.example.com', port: 587, username: 'user', password: 'pass'});
}
describe('SmtpEmailProvider', () => {
beforeEach(() => {
sendMail.mockReset();
sendMail.mockResolvedValue({});
});
it('passes the reply-to address to nodemailer', async () => {
await expect(createProvider().sendEmail({...MESSAGE, replyTo: '[email protected]'})).resolves.toBe(true);
expect(sendMail).toHaveBeenCalledWith({
to: '[email protected]',
from: 'Fluxer <[email protected]>',
replyTo: '[email protected]',
subject: 'Subject',
text: 'Body',
});
});
it('omits the reply-to address when the message has none', async () => {
await expect(createProvider().sendEmail(MESSAGE)).resolves.toBe(true);
expect(sendMail.mock.calls[0][0]).not.toHaveProperty('replyTo');
});
});
@@ -1,14 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createHash} from 'node:crypto';
import {extractMessageTemplatePlaceholders} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
import {EmailI18nService} from '@pkgs/email/src/EmailI18nService';
import type {EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
import {EmailService} from '@pkgs/email/src/EmailService';
import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n';
import {EMAIL_I18N_LOCALE_MESSAGES} from '@pkgs/email/src/email_i18n/EmailI18nLocales';
import {EMAIL_I18N_MESSAGES} from '@pkgs/email/src/email_i18n/EmailI18nMessages';
import type {EmailLegalLinks, EmailTemplateVariables} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
import type {EmailTemplateVariables} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
import type {EmailTemplateKey} from '@pkgs/email/src/email_i18n/EmailI18nTypes.generated';
import {describe, expect, it} from 'vitest';
@@ -100,31 +94,6 @@ const FIXTURE: {[K in EmailTemplateKey]: EmailTemplateVariables[K]} = {
unban_notification: {username: 'testuser', reason: 'Appeal accepted'},
};
const CONTACT_KEYS = [
'account_deletion_cancelled',
'account_deletion_scheduled_inactivity',
'account_deletion_scheduled_requested',
'account_scheduled_deletion',
'account_temp_banned',
'dsa_report_resolved',
'report_resolved',
'scheduled_deletion_notification',
] as const satisfies ReadonlyArray<EmailTemplateKey>;
const SUPPORT_KEYS = [
'gift_chargeback_notification',
'harvest_completed',
'inactivity_warning',
] as const satisfies ReadonlyArray<EmailTemplateKey>;
function renderBody<K extends EmailTemplateKey>(key: K, locale: string, variables: EmailTemplateVariables[K]): string {
const result = getEmailTemplate(key, locale, variables, 'Fluxer');
if (!result.ok) {
throw new Error(result.error.message);
}
return result.value.body;
}
describe('EmailI18n locale files', () => {
it.each(LOCALES)('%s loads without module errors', (locale) => {
const template = getEmailTemplate(
@@ -165,284 +134,4 @@ describe('EmailI18n locale files', () => {
expect(result.value.body, key).not.toContain('Coordinated Universal Time');
}
});
it('renders dates and times in UTC with a zone label', () => {
expect(renderBody('self_deletion_scheduled', 'en-US', {username: 'testuser', deletionDate: DATE})).toContain(
'Thursday, October 1, 2026 at 11:30 PM UTC',
);
});
it.each(['account_deletion_scheduled_requested', 'account_deletion_scheduled_inactivity'] as const)(
'%s makes no enforcement claim',
(key) => {
const body = renderBody(key, 'en-US', {...FIXTURE[key], reason: 'Some reason'});
expect(body).not.toContain('Terms of Service');
expect(body.toLowerCase()).not.toContain('appeal');
expect(body).toContain('Reason: Some reason');
},
);
it.each(['account_deletion_scheduled_requested', 'account_deletion_scheduled_inactivity'] as const)(
'%s leaves no gap without a reason',
(key) => {
const body = renderBody(key, 'en-US', {...FIXTURE[key], reason: null});
expect(body).not.toContain('Reason:');
expect(body).not.toContain('\n\n\n');
},
);
it.each(['en-US', ...LOCALES])('%s names each reported target kind in the receipt', (locale) => {
const bodies = (['message', 'user', 'guild'] as const).map((targetKind) =>
renderBody('report_received', locale, {reportId: '1234567890', targetKind}),
);
expect(new Set(bodies).size).toBe(3);
for (const body of bodies) {
expect(body).toContain('1234567890');
expect(body).not.toContain('targetKind');
}
});
it('renders the receipt for each target kind', () => {
expect(renderBody('report_received', 'en-US', {reportId: '1', targetKind: 'message'})).toContain(
'report about a message on Fluxer.',
);
expect(renderBody('report_received', 'en-US', {reportId: '1', targetKind: 'user'})).toContain(
'report about an account on Fluxer.',
);
expect(renderBody('report_received', 'en-US', {reportId: '1', targetKind: 'guild'})).toContain(
'report about a community on Fluxer.',
);
});
it.each(['en-US', ...LOCALES])('%s tells a DSA reporter how to challenge the decision', (locale) => {
const withComment = renderBody('dsa_report_resolved', locale, {
reportId: '1234567890',
publicComment: 'We removed the content.',
hasComment: 'yes',
appeals_email: '[email protected]',
});
const withoutComment = renderBody('dsa_report_resolved', locale, {
reportId: '1234567890',
publicComment: '',
hasComment: 'no',
appeals_email: '[email protected]',
});
for (const body of [withComment, withoutComment]) {
expect(body).toContain('1234567890');
expect(body).toContain('[email protected]');
expect(body).toContain('60');
expect(body).not.toContain('\n\n\n');
}
expect(withComment).toContain('We removed the content.');
expect(withComment.split('\n\n')).toHaveLength(withoutComment.split('\n\n').length + 1);
});
it.each(['en-US', ...LOCALES])('%s lists only the configured policy links in enforcement notices', (locale) => {
const terms = 'https://example.com/terms';
const guidelines = 'https://example.com/guidelines';
for (const key of ['account_temp_banned', 'account_scheduled_deletion'] as const) {
const render = (termsUrl: string | null, guidelinesUrl: string | null, legalLinks: EmailLegalLinks) =>
renderBody(key, locale, {...FIXTURE[key], termsUrl, guidelinesUrl, legalLinks});
const both = render(terms, guidelines, 'both');
const termsOnly = render(terms, null, 'terms');
const guidelinesOnly = render(null, guidelines, 'guidelines');
const none = render(null, null, 'none');
expect(both, key).toContain(terms);
expect(both, key).toContain(guidelines);
expect(termsOnly, key).toContain(terms);
expect(termsOnly, key).not.toContain(guidelines);
expect(guidelinesOnly, key).toContain(guidelines);
expect(guidelinesOnly, key).not.toContain(terms);
expect(none, key).not.toContain('https://');
const bullets = (body: string) => body.split('\n').filter((line) => line.startsWith('- ')).length;
expect(bullets(both) - bullets(none), key).toBe(2);
expect(bullets(termsOnly) - bullets(none), key).toBe(1);
expect(bullets(guidelinesOnly) - bullets(none), key).toBe(1);
for (const body of [both, termsOnly, guidelinesOnly, none]) {
expect(body, key).not.toContain('\n\n\n');
expect(body, key).not.toContain('legalLinks');
expect(body, key).toContain('[email protected]');
}
expect(termsOnly.split('\n'), key).toHaveLength(both.split('\n').length - 1);
expect(guidelinesOnly.split('\n'), key).toHaveLength(both.split('\n').length - 1);
expect(none.split('\n\n'), key).toHaveLength(both.split('\n\n').length - 1);
}
});
it.each(['en-US', ...LOCALES])('%s leaves no gap in enforcement notices without a reason', (locale) => {
for (const key of ['account_temp_banned', 'account_scheduled_deletion'] as const) {
for (const legalLinks of ['both', 'none'] as const) {
const withReason = renderBody(key, locale, {...FIXTURE[key], legalLinks, reason: 'Repeated spam'});
const withoutReason = renderBody(key, locale, {...FIXTURE[key], legalLinks, reason: null});
expect(withReason, key).toContain('Repeated spam\n\n');
expect(withoutReason, key).not.toContain('Repeated spam');
for (const body of [withReason, withoutReason]) {
expect(body, key).not.toContain('\n\n\n');
expect(body, key).not.toContain('{');
}
expect(withoutReason.split('\n\n'), key).toHaveLength(withReason.split('\n\n').length - 1);
expect(withoutReason.split('\n'), key).toHaveLength(withReason.split('\n').length - 2);
}
}
});
it.each(['en-US', ...LOCALES])('%s names no mailbox when the instance has none', (locale) => {
const english = (key: (typeof CONTACT_KEYS)[number], variables: object) =>
renderBody(key, 'en-US', {...FIXTURE[key], ...variables} as never);
for (const key of CONTACT_KEYS) {
const hosted = renderBody(key, locale, FIXTURE[key] as never);
const neutral = renderBody(key, locale, {...FIXTURE[key], appeals_email: null, safety_email: null} as never);
expect(hosted, key).toMatch(/(appeals|safety)@fluxer\.com/);
expect(neutral, key).not.toContain('@');
expect(neutral, key).not.toMatch(/\bnull\b/);
expect(neutral, key).not.toContain('{');
expect(neutral, key).not.toContain('\n\n\n');
expect(neutral, key).not.toBe(hosted);
expect(neutral.split('\n'), key).toHaveLength(hosted.split('\n').length);
if (locale !== 'en-US' && locale !== 'en-GB') {
const neutralLine = neutral.split('\n').find((line, index) => line !== hosted.split('\n')[index]);
const englishNeutral = english(key, {appeals_email: null, safety_email: null}).split('\n');
expect(neutralLine, key).toBeDefined();
expect(englishNeutral, key).not.toContain(neutralLine);
expect(neutralLine, key).not.toContain('the administrators of this instance');
}
}
});
it.each(['en-US', ...LOCALES])('%s points at the instance administrators instead of a support team', (locale) => {
for (const key of SUPPORT_KEYS) {
const hosted = renderBody(key, locale, FIXTURE[key] as never);
const neutral = renderBody(key, locale, {...FIXTURE[key], support_email: null} as never);
const hostedLines = hosted.split('\n');
const neutralLines = neutral.split('\n');
expect(neutralLines, key).toHaveLength(hostedLines.length);
const changed = neutralLines.filter((line, index) => line !== hostedLines[index]);
expect(changed, key).toHaveLength(1);
expect(neutral, key).not.toMatch(/\bnull\b/);
expect(neutral, key).not.toContain('{');
expect(neutral, key).not.toContain('@');
expect(changed[0], key).not.toMatch(/support/i);
if (locale !== 'en-US' && locale !== 'en-GB') {
const englishNeutral = renderBody(key, 'en-US', {...FIXTURE[key], support_email: null} as never).split('\n');
expect(englishNeutral, key).not.toContain(changed[0]);
expect(changed[0], key).not.toContain('the administrators of this instance');
}
}
});
it.each(['en-US', ...LOCALES])('%s leaves no gap in deletion and unban notices without a reason', (locale) => {
for (const key of ['scheduled_deletion_notification', 'unban_notification'] as const) {
const withReason = renderBody(key, locale, {...FIXTURE[key], reason: 'Repeated spam'});
const withoutReason = renderBody(key, locale, {...FIXTURE[key], reason: null});
expect(withReason, key).toContain('Repeated spam\n\n');
expect(withoutReason, key).not.toContain('Repeated spam');
for (const body of [withReason, withoutReason]) {
expect(body, key).not.toContain('\n\n\n');
}
expect(withoutReason.split('\n\n'), key).toHaveLength(withReason.split('\n\n').length - 1);
}
});
it('greets a DSA reporter without a username', () => {
for (const key of ['report_received', 'dsa_report_resolved'] as const) {
const result = getEmailTemplate(key, 'en-US', FIXTURE[key], 'Fluxer');
expect(result.ok).toBe(true);
if (!result.ok) continue;
expect(result.value.body.startsWith('Hello,\n\n')).toBe(true);
expect(result.value.body).not.toContain('{');
}
});
it('renders a blank reason the same as no reason', async () => {
const sent: Array<EmailMessage> = [];
const provider: IEmailProvider = {
sendEmail: async (message) => {
sent.push(message);
return true;
},
};
const service = new EmailService(
{
enabled: true,
fromEmail: '[email protected]',
fromName: 'Fluxer',
appBaseUrl: 'https://example.com',
termsUrl: 'https://example.com/terms',
guidelinesUrl: 'https://example.com/guidelines',
appealsEmail: '[email protected]',
safetyEmail: '[email protected]',
supportEmail: '[email protected]',
productName: 'Fluxer',
},
new EmailI18nService(),
provider,
);
await service.sendUnbanNotification('[email protected]', 'testuser', ' ', 'en-US');
await service.sendUnbanNotification('[email protected]', 'testuser', null, 'en-US');
await service.sendAccountDeletionRequestedEmail('[email protected]', 'testuser', ' ', DATE, 'en-US');
await service.sendAccountDeletionRequestedEmail('[email protected]', 'testuser', null, DATE, 'en-US');
expect(sent).toHaveLength(4);
expect(sent[0].text).toBe(sent[1].text);
expect(sent[0].text).not.toContain('Reason:');
expect(sent[2].text).toBe(sent[3].text);
expect(sent[2].text).not.toContain('Reason:');
});
});
const HOSTED_RENDER_SHA256 = {
email_verification: {
'en-US': 'ea69bd530e1f9cf7cd04bc88d5c0a032069b2538bc5138eeee5bcea7dd21f539',
de: 'beb941104326bf73a523de0ccd33b78d3b531f461eea6ce1fae37a88634e69fa',
ja: 'e928c0d57baf6c1efd055d88661f6dc27f24119b0b972cafe614fed40663efb1',
},
password_reset: {
'en-US': 'b37a3491ef9126f251fd01fe2aa642891eb50aa56c23c36d0d62ccd0a9a7785d',
de: '148b9fbb52845befb1b9f24a063dafe2d5f000abc2104410d4e943ce56191917',
ja: 'd4198d68c126ad796954010431fe966b4df2b2fbabb9b4ddd5da0fbfd3b645a5',
},
ip_authorization: {
'en-US': '073ead7d3e270c886b44db16266f6cfb4a5b8b182d279a8bf17ddea49198b415',
de: 'd1c38f6a791a87379ca5b67e270d66e3741db314933b0b68c10bb9aab1781de1',
ja: 'adc16933f5efda8b176d7348942efaf6b2bff16bd09f14de9a48d727a76f011f',
},
gift_chargeback_notification: {
'en-US': '4ec08598e9087578ba52aa2490a1198dbfe68dd3bd496f4175b3b8ce533a01ac',
de: 'dd5a7f572a9063e26e5effa05193185286e28b5e6e256f3d0a98b97e7e31e952',
ja: 'c8d3a0fce144d278303f3d36c919d17bd60ab0738578f7e0f3e15e79bdb2bf19',
},
unban_notification: {
'en-US': '0cca736b44b8e1839bf3a1c9c8720c915ea80f40596801c57a9809c40c35c206',
de: '926e3762019888815da2ce7c70e94661878b9fba417bf0ffb11836abb2b74904',
ja: 'f5546b66f61d992aff2367c7b930d7683cd7a37891a3c2a069a8de2769113bd6',
},
} as const satisfies Partial<Record<EmailTemplateKey, Record<'en-US' | 'de' | 'ja', string>>>;
function renderEmail(key: EmailTemplateKey, locale: string, productName: string): {subject: string; body: string} {
const result = getEmailTemplate(key, locale, FIXTURE[key], productName);
if (!result.ok) {
throw new Error(result.error.message);
}
return result.value;
}
describe('EmailI18n product name', () => {
it('renders the hosted name byte for byte as before the name became a parameter', () => {
for (const [key, locales] of Object.entries(HOSTED_RENDER_SHA256)) {
for (const [locale, expected] of Object.entries(locales)) {
const {subject, body} = renderEmail(key as EmailTemplateKey, locale, 'Fluxer');
const actual = createHash('sha256').update(`${subject}\n${body}`).digest('hex');
expect(actual, `${key} ${locale}`).toBe(expected);
}
}
});
it.each(['en-US', ...LOCALES])('%s names the instance and never Fluxer in every template', (locale) => {
for (const key of TEMPLATE_KEYS) {
const {subject, body} = renderEmail(key, locale, 'Example Chat');
const rendered = `${subject}\n${body}`;
expect(rendered, key).toContain('Example Chat');
expect(rendered, key).not.toContain('Fluxer');
expect(rendered, key).not.toContain('{product_name}');
}
});
it.each(['en-US', ...LOCALES])('%s differs from the hosted render only by the name', (locale) => {
for (const key of TEMPLATE_KEYS) {
const hosted = renderEmail(key, locale, 'Fluxer');
const selfHosted = renderEmail(key, locale, 'Example Chat');
expect(selfHosted.subject.replaceAll('Example Chat', 'Fluxer'), key).toBe(hosted.subject);
expect(selfHosted.body.replaceAll('Example Chat', 'Fluxer'), key).toBe(hosted.body);
}
});
it('prints a name with message syntax characters as written', () => {
const {subject, body} = renderEmail('password_reset', 'en-US', "Bob's {Chat} #1");
expect(`${subject}\n${body}`).toContain("Bob's {Chat} #1");
});
});