/** * Tests: Gmail-Delete-Strategie — MOVE statt EXPUNGE * * scan-internal.post.ts wählt den Delete-Pfad anhand des IMAP-Hosts: * - imap.gmail.com → messageMove() in Trash-Folder (MOVE) * - alle anderen → messageDelete() + EXPUNGE-Fallback * * Diese Tests validieren die Auswahllogik als Pure-Logic-Unit, * ohne echten IMAP-Connect. Mock-IMAP simuliert Aufrufverhalten. * * DSGVO: keine PII. Synthetic User-IDs (uuid-Format), keine echten E-Mails. */ import { describe, it, expect, vi } from "vitest"; // ─── Mock-IMAP-Factory ──────────────────────────────────────────────────────── function makeImapMock({ trashSpecialUsePath, }: { trashSpecialUsePath?: string; } = {}) { const mailboxes = [ { path: "INBOX", flags: new Set(), specialUse: undefined }, { path: trashSpecialUsePath ?? "[Gmail]/Trash", flags: new Set(), specialUse: "\\Trash", }, { path: "[Gmail]/All Mail", flags: new Set(), specialUse: "\\All" }, ]; return { connect: vi.fn().mockResolvedValue(undefined), logout: vi.fn().mockResolvedValue(undefined), list: vi.fn().mockResolvedValue(mailboxes), getMailboxLock: vi.fn().mockResolvedValue({ release: vi.fn() }), status: vi.fn().mockResolvedValue({ messages: 0 }), fetchAll: vi.fn().mockResolvedValue([]), messageMove: vi.fn().mockResolvedValue(undefined), messageDelete: vi.fn().mockResolvedValue(undefined), messageFlagsAdd: vi.fn().mockResolvedValue(undefined), expunge: vi.fn().mockResolvedValue(undefined), _mailboxes: mailboxes, }; } // ─── Auswahllogik als Pure Function (extrahiert aus scan-internal) ──────────── // // Die eigentliche Logik sitzt in scan-internal.post.ts welches Nitro-Globals // braucht und daher nicht direkt importiert werden kann. Wir testen die // Entscheidungslogik als isolierte Funktion — identisch zum Produktionscode. type MailboxEntry = { path: string; specialUse?: string }; function resolveTrashFolder(mailboxes: MailboxEntry[]): string { const trashMailbox = mailboxes.find((mb) => mb.specialUse === "\\Trash"); return trashMailbox?.path ?? "[Gmail]/Trash"; } async function performDelete(opts: { isGmail: boolean; uids: string[]; imap: ReturnType; mailboxes: MailboxEntry[]; }): Promise { const { isGmail, uids, imap, mailboxes } = opts; if (uids.length === 0) return; if (isGmail) { const trashFolder = resolveTrashFolder(mailboxes); await imap.messageMove(uids.join(","), trashFolder, { uid: true }); } else { await imap.messageDelete(uids.join(","), { uid: true }); } } // ─── Tests ──────────────────────────────────────────────────────────────────── describe("Gmail-Delete-Strategie: MOVE statt EXPUNGE", () => { it("Gmail-Host → messageMove() wird gerufen, messageDelete() NICHT", async () => { const imap = makeImapMock(); await performDelete({ isGmail: true, uids: ["42", "43"], imap, mailboxes: imap._mailboxes, }); expect(imap.messageMove).toHaveBeenCalledOnce(); expect(imap.messageDelete).not.toHaveBeenCalled(); }); it("Gmail: MOVE wird mit korrekten UIDs gerufen (komma-separiert)", async () => { const imap = makeImapMock(); await performDelete({ isGmail: true, uids: ["10", "11", "12"], imap, mailboxes: imap._mailboxes, }); expect(imap.messageMove).toHaveBeenCalledWith("10,11,12", expect.any(String), { uid: true }); }); it("Gmail mit specialUse='\\\\Trash' → Trash-Pfad via specialUse aufgelöst (kein Hardcode)", async () => { // Simuliert deutschen Gmail: Trash heißt '[Gmail]/Papierkorb' const imap = makeImapMock({ trashSpecialUsePath: "[Gmail]/Papierkorb" }); await performDelete({ isGmail: true, uids: ["99"], imap, mailboxes: imap._mailboxes, }); const [, targetFolder] = imap.messageMove.mock.calls[0]; expect(targetFolder).toBe("[Gmail]/Papierkorb"); }); it("Gmail ohne specialUse='\\\\Trash' in Mailbox-Liste → Fallback '[Gmail]/Trash'", async () => { // Edge-Case: kein Mailbox mit specialUse='\\Trash' vorhanden const mailboxesNoTrash: MailboxEntry[] = [ { path: "INBOX", specialUse: undefined }, { path: "[Gmail]/All Mail", specialUse: "\\All" }, ]; const imap = makeImapMock(); await performDelete({ isGmail: true, uids: ["55"], imap, mailboxes: mailboxesNoTrash, }); const [, targetFolder] = imap.messageMove.mock.calls[0]; expect(targetFolder).toBe("[Gmail]/Trash"); }); it("Nicht-Gmail (iCloud/Outlook/IONOS) → messageDelete() wird gerufen, messageMove() NICHT", async () => { const imap = makeImapMock(); await performDelete({ isGmail: false, uids: ["1", "2"], imap, mailboxes: imap._mailboxes, }); expect(imap.messageDelete).toHaveBeenCalledOnce(); expect(imap.messageMove).not.toHaveBeenCalled(); }); it("Leere UIDs → weder MOVE noch DELETE gerufen", async () => { const imap = makeImapMock(); await performDelete({ isGmail: true, uids: [], imap, mailboxes: imap._mailboxes, }); expect(imap.messageMove).not.toHaveBeenCalled(); expect(imap.messageDelete).not.toHaveBeenCalled(); }); }); // ─── resolveTrashFolder() — Unit-Tests ─────────────────────────────────────── describe("resolveTrashFolder()", () => { it("Mailbox mit specialUse='\\\\Trash' → deren path wird zurückgegeben", () => { const mailboxes: MailboxEntry[] = [ { path: "INBOX" }, { path: "[Gmail]/Trash", specialUse: "\\Trash" }, { path: "[Gmail]/All Mail", specialUse: "\\All" }, ]; expect(resolveTrashFolder(mailboxes)).toBe("[Gmail]/Trash"); }); it("Deutschen Gmail: '[Gmail]/Papierkorb' als specialUse-Trash", () => { const mailboxes: MailboxEntry[] = [ { path: "INBOX" }, { path: "[Gmail]/Papierkorb", specialUse: "\\Trash" }, ]; expect(resolveTrashFolder(mailboxes)).toBe("[Gmail]/Papierkorb"); }); it("Kein Trash-Mailbox → Fallback '[Gmail]/Trash'", () => { const mailboxes: MailboxEntry[] = [ { path: "INBOX" }, { path: "[Gmail]/All Mail", specialUse: "\\All" }, ]; expect(resolveTrashFolder(mailboxes)).toBe("[Gmail]/Trash"); }); it("Leere Mailbox-Liste → Fallback '[Gmail]/Trash'", () => { expect(resolveTrashFolder([])).toBe("[Gmail]/Trash"); }); });