Golden-master tests pinning the CURRENT behavior of high-value modules that had no coverage — integration seams, security helpers, two API route handlers, and complex pure utils. 111 tests across 12 files. New tests: - auth-crypto / session-cookie: AES-256-GCM session encryption roundtrip, tamper/version/missing-secret handling; cookie-slot naming. - unified-mailbox: multi-account fan-out, sort, totals, per-account error isolation, personal-vs-shared JMAP target resolution, counts/roles. - account-state-manager: snapshot/restore across the six real Zustand stores; clearAllStores reset shape; evict. - mdn: RFC 5322 MDN assembly (CRLF, RFC2047, base64 wrap, headers). - tnef: winmail.dat binary parsing from hand-built fixtures. - download-filename / subject-prefix / birthday-calendar / eml-import: filename templating, multilingual prefix stripping, birthday event generation, .eml/.zip import. - webdav / caldav-discover route handlers: auth guards, path validation, upstream URL construction, candidate probing. - helpers/factories.ts: shared makeEmail/makeMailbox/makeFakeJmapClient. Tests follow the repo's existing patterns (route-import, fake IJMAPClient, fetch spy, real store singletons). Where current behavior looks buggy it is pinned and flagged with a // CHARACTERISATION: comment (see PR for the suspected-bugs list); no production code is changed.
128 lines
5.0 KiB
TypeScript
128 lines
5.0 KiB
TypeScript
// Pin TZ so the local-time date rendering in dateParts is deterministic.
|
|
process.env.TZ = 'UTC';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import type { Email } from '@/lib/jmap/types';
|
|
import {
|
|
emailExportFilename,
|
|
attachmentDownloadFilename,
|
|
bundleExportFilename,
|
|
emailVars,
|
|
attachmentVars,
|
|
buildSampleEmail,
|
|
} from '@/lib/download-filename';
|
|
|
|
const makeEmail = (over: Partial<Email>): Email =>
|
|
({ id: 'e', receivedAt: '2026-05-22T14:05:33Z', from: [], to: [], subject: '', ...over } as unknown as Email);
|
|
|
|
describe('emailExportFilename', () => {
|
|
it('renders the default template from the sample email (UTC)', () => {
|
|
expect(emailExportFilename(buildSampleEmail())).toBe(
|
|
'2026-05-22 14.05.33 (Alice Sender-Bob Recipient) Benachrichtigung von Ihrem Gerät.eml',
|
|
);
|
|
});
|
|
|
|
it('applies lowercase + stripDiacritics + underscore-spaces transforms', () => {
|
|
expect(
|
|
emailExportFilename(buildSampleEmail(), {
|
|
template: '{from_name}-{subject}',
|
|
lowercase: true,
|
|
stripDiacritics: true,
|
|
spaceReplacement: 'underscore',
|
|
}),
|
|
).toBe('alice_sender-benachrichtigung_von_ihrem_gerat.eml');
|
|
});
|
|
|
|
it('falls back to "no subject" for an empty subject', () => {
|
|
expect(emailExportFilename(makeEmail({ subject: '' }), '{subject}')).toBe('no subject.eml');
|
|
});
|
|
|
|
it('falls back to "email" when the template renders empty', () => {
|
|
expect(emailExportFilename(makeEmail({}), '{unknown_token}')).toBe('email.eml');
|
|
});
|
|
|
|
it('CHARACTERISATION: per-token sanitise caps each value at 80 chars', () => {
|
|
// sanitizePart defaults to maxLen=80, applied per {token} during render —
|
|
// so a single long {subject} is truncated to 80 well before the 200 cap.
|
|
const out = emailExportFilename(makeEmail({ subject: 'a'.repeat(300) }), '{subject}');
|
|
expect(out).toBe('a'.repeat(80) + '.eml');
|
|
});
|
|
});
|
|
|
|
describe('attachmentDownloadFilename', () => {
|
|
it('email===null: sanitises the raw attachment name, applying transforms', () => {
|
|
expect(attachmentDownloadFilename(null, { name: 'Report.PDF' })).toBe('Report.PDF');
|
|
expect(attachmentDownloadFilename(null, { name: 'Report.PDF' }, { lowercase: true })).toBe('report.pdf');
|
|
});
|
|
|
|
it('{filename} token preserves the original extension', () => {
|
|
expect(
|
|
attachmentDownloadFilename(buildSampleEmail(), { name: 'My Report.pdf' }, '{filename}'),
|
|
).toBe('My Report.pdf');
|
|
});
|
|
|
|
it('template without {ext}/{filename} appends the attachment extension', () => {
|
|
expect(
|
|
attachmentDownloadFilename(buildSampleEmail(), { name: 'My Report.PDF' }, '{name}'),
|
|
).toBe('My Report.PDF');
|
|
expect(
|
|
attachmentDownloadFilename(buildSampleEmail(), { name: 'My Report.PDF' }, { template: '{name}', lowercase: true }),
|
|
).toBe('my report.pdf');
|
|
});
|
|
|
|
it('attachment with no extension yields no trailing dot', () => {
|
|
expect(attachmentDownloadFilename(buildSampleEmail(), { name: 'noext' }, '{name}')).toBe('noext');
|
|
});
|
|
|
|
it('sanitises path-traversal characters out of the name', () => {
|
|
const out = attachmentDownloadFilename(null, { name: '../../etc/passwd' });
|
|
expect(out).not.toContain('/');
|
|
// CHARACTERISATION: slashes → "_", then the leading "._-" run is stripped,
|
|
// so "../../etc/passwd" collapses to "etc_passwd".
|
|
expect(out).toBe('etc_passwd');
|
|
});
|
|
});
|
|
|
|
describe('bundleExportFilename', () => {
|
|
it('substitutes {count} and appends .zip', () => {
|
|
expect(bundleExportFilename(3, '{count}-emails', '2026-05-22T14:05:33Z')).toBe('3-emails.zip');
|
|
});
|
|
|
|
it('uses the default template', () => {
|
|
expect(bundleExportFilename(5, {}, '2026-05-22T14:05:33Z')).toBe('emails-5.zip');
|
|
});
|
|
});
|
|
|
|
describe('emailVars (date + address labels)', () => {
|
|
it('returns the invalid-date sentinel for an unparseable date', () => {
|
|
const v = emailVars(makeEmail({ receivedAt: 'not-a-date', sentAt: undefined }));
|
|
expect(v.date).toBe('0000-00-00 00.00.00');
|
|
expect(v.date_short).toBe('0000-00-00');
|
|
expect(v.time).toBe('00.00.00');
|
|
expect(v.year).toBe('0000');
|
|
});
|
|
|
|
it('addrLabel falls back name → email user-part → "unknown"', () => {
|
|
expect(emailVars(makeEmail({ from: [{ email: 'alice@example.com' }] }) ).from).toBe('alice');
|
|
expect(emailVars(makeEmail({ from: [] })).from).toBe('unknown');
|
|
expect(emailVars(makeEmail({ from: [{ name: ' ', email: 'x@y.com' }] })).from).toBe('x');
|
|
});
|
|
});
|
|
|
|
describe('attachmentVars (extension split)', () => {
|
|
it('splits name and ext on the last dot', () => {
|
|
const v = attachmentVars(buildSampleEmail(), { name: 'doc.tar.gz' });
|
|
expect(v).toMatchObject({ filename: 'doc.tar.gz', name: 'doc.tar', ext: 'gz' });
|
|
});
|
|
|
|
it('treats a dotless name as having no extension', () => {
|
|
const v = attachmentVars(buildSampleEmail(), { name: 'noext' });
|
|
expect(v).toMatchObject({ name: 'noext', ext: '' });
|
|
});
|
|
|
|
it('defaults a missing name to "attachment"', () => {
|
|
const v = attachmentVars(buildSampleEmail(), {});
|
|
expect(v).toMatchObject({ filename: 'attachment', ext: '' });
|
|
});
|
|
});
|