test(quick-260921-oxm): rote Tests fuer die beiden IMAP-Befunde

- B-06: prueft die an ImapFlow uebergebenen Optionen fuer starttls und ssl-tls
- B-05: prueft, dass ein octet-stream-Anhang am Dateinamen aus
  Content-Disposition erkannt wird
- Einhaengen des Testdoppels in einen Helfer gezogen; die Umdeutung des
  Konstruktors steht damit nur noch an einer Stelle statt an zwoelf

Gegen den heutigen Stand rot: 3 von 12 Faellen scheitern
(doSTARTTLS undefined, Feld requireTLS vorhanden, Anhang nicht eingesammelt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
2026-09-21 18:00:53 +02:00
parent f23671ac6c
commit 7691d1fd6d
+147 -7
View File
@@ -72,6 +72,17 @@ function makeMockClient(overrides: Partial<Record<string, unknown>> = {}) {
};
}
/**
* Haengt ein Testdoppel als ImapFlow-Klient ein.
*
* Der Modul-Mock oben ersetzt den Konstruktor durch `vi.fn()`; diese Funktion
* ist die einzige Stelle im Test, die das ausnutzt. Vorher stand dieselbe
* Umdeutung in jedem einzelnen Fall.
*/
function useMockClient(client: ReturnType<typeof makeMockClient>): void {
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
}
beforeEach(() => {
vi.clearAllMocks();
});
@@ -79,7 +90,7 @@ beforeEach(() => {
describe('ImapProvider.fetchMessages', () => {
it('returns one InboxMessage with bodyHtml from the html part and bodyText from the plain part', async () => {
const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -97,7 +108,7 @@ describe('ImapProvider.fetchMessages', () => {
it('marks each processed message \\Seen (idempotency for re-polls)', async () => {
const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
await provider.fetchMessages(BASE_CONFIG);
@@ -107,7 +118,7 @@ describe('ImapProvider.fetchMessages', () => {
it('honors the same UNSEEN + optional senderFilter search as fetchPdfAttachments', async () => {
const client = makeMockClient();
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
await provider.fetchMessages({ ...BASE_CONFIG, senderFilter: 'vergabeportal.de' });
@@ -122,7 +133,7 @@ describe('ImapProvider.fetchMessages', () => {
const client = makeMockClient({
connect: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')),
});
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]);
@@ -132,7 +143,7 @@ describe('ImapProvider.fetchMessages', () => {
const client = makeMockClient({
search: vi.fn().mockRejectedValue(new Error('search boom')),
});
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
await expect(provider.fetchMessages(BASE_CONFIG)).resolves.toEqual([]);
@@ -141,7 +152,7 @@ describe('ImapProvider.fetchMessages', () => {
it('returns [] when there are no unread messages', async () => {
const client = makeMockClient({ search: vi.fn().mockResolvedValue([]) });
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -164,7 +175,7 @@ describe('ImapProvider.fetchMessages', () => {
},
]),
});
(ImapFlow as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => client);
useMockClient(client);
const provider = new ImapProvider();
const messages = await provider.fetchMessages(BASE_CONFIG);
@@ -174,3 +185,132 @@ describe('ImapProvider.fetchMessages', () => {
expect(messages[0]!.bodyText).toBe('');
});
});
/**
* Befund B-06 (gemeldet in 260921-m34): buildClient() uebergab `requireTLS`,
* eine Option, die imapflow 1.4.3 gar nicht kennt — weder in `ImapFlowOptions`
* (lib/imap-flow.d.ts) noch im Laufzeitcode (lib/imap-flow.js). Sie wurde still
* verworfen, ein STARTTLS-Zwang entstand durch sie nie. Die richtige Option
* heisst `doSTARTTLS` (imap-flow.d.ts:81).
*
* Geprueft wird hier ausschliesslich, was an `new ImapFlow(...)` uebergeben
* wird — keine echte Verbindung.
*/
describe('ImapProvider — Transportverschluesselung (B-06)', () => {
/** Optionen des zuletzt erzeugten ImapFlow-Klienten. */
function lastClientOptions() {
const calls = vi.mocked(ImapFlow).mock.calls;
return calls[calls.length - 1]?.[0];
}
it('erzwingt STARTTLS, wenn die Verschluesselung auf starttls steht', async () => {
const client = makeMockClient();
useMockClient(client);
const provider = new ImapProvider();
await provider.testConnection({ ...BASE_CONFIG, port: 143, encryption: 'starttls' });
const options = lastClientOptions();
expect(options?.secure).toBe(false);
expect(options?.doSTARTTLS).toBe(true);
expect(options).not.toHaveProperty('requireTLS');
});
it('setzt doSTARTTLS nicht auf true, wenn die Verschluesselung auf ssl-tls steht', async () => {
const client = makeMockClient();
useMockClient(client);
const provider = new ImapProvider();
await provider.testConnection({ ...BASE_CONFIG, encryption: 'ssl-tls' });
const options = lastClientOptions();
// imapflow wirft bei secure=true zusammen mit doSTARTTLS=true
// ("Misconfiguration", imap-flow.js:1201) — diese Kombination darf nie entstehen.
expect(options?.secure).toBe(true);
expect(options?.doSTARTTLS).not.toBe(true);
expect(options).not.toHaveProperty('requireTLS');
});
});
/**
* Befund B-05 (gemeldet in 260921-m34): der Dateiname aus Content-Disposition
* wurde als `disposition.parameters.filename` gelesen. imapflow deklariert
* `disposition` aber als Zeichenkette (imap-flow.d.ts:448) und legt die
* Parameter in ein eigenes Feld `dispositionParameters` (:450, gefuellt in
* tools.js:887 mit kleingeschriebenen Schluesseln). Der alte Ausdruck war zur
* Laufzeit immer undefined — Anhaenge, die als application/octet-stream
* ankommen (typisch fuer Outlook), wurden darueber nie erkannt.
*/
describe('ImapProvider.fetchPdfAttachments — Dateiname aus Content-Disposition (B-05)', () => {
function makeAttachmentClient(bodyStructure: unknown) {
return makeMockClient({
search: vi.fn().mockResolvedValue([7]),
fetchAll: vi.fn().mockResolvedValue([
{
uid: 7,
envelope: {
messageId: '<msg-7@example.com>',
subject: 'Rechnung',
from: [{ address: 'rechnung@dkv.de' }],
date: new Date('2026-07-20T08:00:00Z'),
},
bodyStructure,
},
]),
download: vi.fn(async () => ({ content: makeReadable('%PDF-1.4 inhalt') })),
});
}
it('erkennt einen application/octet-stream-Anhang am Dateinamen aus dispositionParameters', async () => {
const client = makeAttachmentClient({
type: 'multipart/mixed',
childNodes: [
{ type: 'text/plain', part: '1' },
{
type: 'application/octet-stream',
part: '2',
disposition: 'attachment',
dispositionParameters: { filename: 'Rechnung-4711.PDF' },
},
],
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toHaveLength(1);
expect(emails[0]?.attachments ?? []).toHaveLength(1);
expect(emails[0]?.attachments?.[0]?.contentType).toBe('application/pdf');
});
it('erkennt einen application/octet-stream-Anhang weiterhin am Namen aus Content-Type', async () => {
const client = makeAttachmentClient({
type: 'application/octet-stream',
part: '1',
parameters: { name: 'Rechnung-4711.pdf' },
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toHaveLength(1);
expect(emails[0]?.attachments ?? []).toHaveLength(1);
});
it('sammelt einen application/octet-stream-Anhang ohne .pdf-Dateinamen nicht ein', async () => {
const client = makeAttachmentClient({
type: 'application/octet-stream',
part: '1',
disposition: 'attachment',
dispositionParameters: { filename: 'Lieferschein.docx' },
});
useMockClient(client);
const provider = new ImapProvider();
const emails = await provider.fetchPdfAttachments(BASE_CONFIG);
expect(emails).toEqual([]);
});
});