Files
SRCmail/lib/__tests__/tnef.test.ts
T
Stefan HildebrandtandLinus Rath 2fac6ebfb8 test: add characterisation tests for untested integration seams
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.
2026-06-19 23:52:42 +02:00

113 lines
4.2 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { parseTnef, isTnefAttachment } from '@/lib/tnef';
vi.mock('@/lib/debug', () => ({
debug: { group: () => {}, groupEnd: () => {}, log: () => {}, warn: () => {}, table: () => {} },
}));
// ── tiny little-endian writer mirroring the parser's BinaryReader ─────────────
class W {
private bytes: number[] = [];
u8(n: number) { this.bytes.push(n & 0xff); return this; }
u16(n: number) { return this.u8(n).u8(n >>> 8); }
u32(n: number) { return this.u8(n).u8(n >>> 8).u8(n >>> 16).u8(n >>> 24); }
raw(arr: number[]) { for (const b of arr) this.u8(b); return this; }
build() { return new Uint8Array(this.bytes); }
}
const TNEF_SIGNATURE = 0x223e9f78;
const LVL_MESSAGE = 0x01;
const LVL_ATTACHMENT = 0x02;
const attBody = 0x0002800c;
const attMAPIProps = 0x00069003;
const attAttachRenddata = 0x00069002;
const attAttachData = 0x0006800f;
const attAttachTitle = 0x00018010;
const PR_BODY_HTML = 0x1013;
const PT_STRING8 = 0x001e;
const enc = (s: string) => [...new TextEncoder().encode(s)];
function header(w: W) { return w.u32(TNEF_SIGNATURE).u16(0); }
function record(w: W, level: number, attrID: number, data: number[]) {
w.u8(level).u32(attrID).u32(data.length).raw(data).u16(0); // ...data + 2-byte checksum
return w;
}
/** One length-prefixed STRING8 MAPI property block. */
function mapiStringProp(propID: number, str: string): number[] {
const w = new W();
w.u32(1); // property count
w.u16(PT_STRING8).u16(propID);
const val = enc(str);
w.u32(1); // value count
w.u32(val.length).raw(val);
for (let i = 0; i < (4 - (val.length % 4)) % 4; i++) w.u8(0); // pad to 4
return [...w.build()];
}
describe('parseTnef', () => {
it('returns an empty result for data smaller than 6 bytes', () => {
expect(parseTnef(new Uint8Array([1, 2, 3]))).toEqual({ body: null, htmlBody: null, attachments: [] });
});
it('returns an empty result for an invalid signature', () => {
const buf = new W().u32(0xdeadbeef).u16(0).build();
expect(parseTnef(buf)).toEqual({ body: null, htmlBody: null, attachments: [] });
});
it('extracts a plain-text body from an attBody attribute', () => {
const w = header(new W());
record(w, LVL_MESSAGE, attBody, enc('Hello body'));
const r = parseTnef(w.build());
expect(r.body).toBe('Hello body');
expect(r.htmlBody).toBeNull();
expect(r.attachments).toEqual([]);
});
it('extracts an HTML body from PR_BODY_HTML in MAPI props', () => {
const w = header(new W());
record(w, LVL_MESSAGE, attMAPIProps, mapiStringProp(PR_BODY_HTML, '<b>hi</b>'));
const r = parseTnef(w.build());
expect(r.htmlBody).toBe('<b>hi</b>'); // trailing NUL stripped
});
it('assembles an attachment from renddata + title + data', () => {
const w = header(new W());
record(w, LVL_ATTACHMENT, attAttachRenddata, [0, 0]); // start attachment
record(w, LVL_ATTACHMENT, attAttachTitle, enc('file.txt'));
record(w, LVL_ATTACHMENT, attAttachData, [1, 2, 3, 4]);
const r = parseTnef(w.build());
expect(r.attachments).toHaveLength(1);
expect(r.attachments[0].name).toBe('file.txt');
expect(r.attachments[0].mimeType).toBe('application/octet-stream');
expect([...r.attachments[0].data]).toEqual([1, 2, 3, 4]);
});
it('stops at a truncated attribute (declared length exceeds remaining)', () => {
// sig + header, then an attribute header claiming 999 bytes with no payload.
const w = header(new W());
w.u8(LVL_MESSAGE).u32(attBody).u32(999);
const r = parseTnef(w.build());
expect(r).toEqual({ body: null, htmlBody: null, attachments: [] });
});
});
describe('isTnefAttachment', () => {
it('matches winmail.dat by name (case-insensitive)', () => {
expect(isTnefAttachment('winmail.dat')).toBe(true);
expect(isTnefAttachment('WinMail.DAT')).toBe(true);
});
it('matches the ms-tnef MIME types', () => {
expect(isTnefAttachment('x', 'application/ms-tnef')).toBe(true);
expect(isTnefAttachment('x', 'application/vnd.ms-tnef')).toBe(true);
});
it('returns false for ordinary attachments and missing values', () => {
expect(isTnefAttachment('document.pdf', 'application/pdf')).toBe(false);
expect(isTnefAttachment()).toBe(false);
});
});