/** * Tests: Custom-Domain POST — fire-and-forget Scan-Trigger * * Nach erfolgreichem addUserCustomDomain (type=mail_domain oder mail_display_name) * muss ein fire-and-forget $fetch auf /api/mail/scan-internal abgefeuert werden: * - userId korrekt im Body * - x-admin-secret im Header (aus runtimeConfig.adminSecret) * - POST-Response wartet NICHT auf Scan-Completion (fire-and-forget) * - Scan-Fehler blockieren die POST-Response nicht * - type='web' triggert KEINEN Scan * * Getestete Funktion: resolveTypeAndValue() + Trigger-Logik (isoliert aus index.post.ts). * * DSGVO: keine PII. Synthetic User-IDs (uuid-Format), synthetic Domains. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; // ─── $fetch Mock ────────────────────────────────────────────────────────────── // Nitro's $fetch wird als global injiziert. Wir mocken es im globalThis // bevor die Logik läuft. let fetchMock: ReturnType; function setupFetchMock(resolveWith: unknown = { ok: true }) { fetchMock = vi.fn().mockResolvedValue(resolveWith); (globalThis as Record).$fetch = fetchMock; } function teardownFetchMock() { delete (globalThis as Record).$fetch; } // ─── Trigger-Logik (isoliert aus index.post.ts) ─────────────────────────────── // Produktionscode ist in index.post.ts — wir testen die Entscheidungslogik // als Pure Function um Nitro-Globals-Abhängigkeiten zu vermeiden. type CustomDomainType = "web" | "mail_domain" | "mail_display_name"; async function triggerScanIfMailDomain(opts: { type: CustomDomainType; userId: string; adminSecret: string; }): Promise { const { type, userId, adminSecret } = opts; if (type === "mail_domain" || type === "mail_display_name") { const globalFetch = (globalThis as Record).$fetch as ( url: string, opts: unknown, ) => Promise; globalFetch("/api/mail/scan-internal", { method: "POST", headers: { "x-admin-secret": adminSecret }, body: { userId }, }).catch((err: unknown) => { console.warn(`[custom-domains] post-add scan-trigger failed for user ${userId}:`, err); }); // Intentional: kein await — fire-and-forget } } // ─── Tests ──────────────────────────────────────────────────────────────────── describe("Custom-Domain POST — Scan-Trigger nach mail_domain-Add", () => { const syntheticUserId = "00000000-0000-0000-0000-000000000001"; const syntheticAdminSecret = "test-admin-secret-xyz"; beforeEach(() => { setupFetchMock(); }); afterEach(() => { teardownFetchMock(); vi.clearAllMocks(); }); it("type=mail_domain → $fetch auf /api/mail/scan-internal wird gerufen", async () => { await triggerScanIfMailDomain({ type: "mail_domain", userId: syntheticUserId, adminSecret: syntheticAdminSecret, }); // Kurz warten damit fire-and-forget Promise im gleichen Tick landet await Promise.resolve(); expect(fetchMock).toHaveBeenCalledOnce(); expect(fetchMock).toHaveBeenCalledWith( "/api/mail/scan-internal", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "x-admin-secret": syntheticAdminSecret, }), body: expect.objectContaining({ userId: syntheticUserId }), }), ); }); it("type=mail_display_name → $fetch wird gerufen (future v1.1 path)", async () => { await triggerScanIfMailDomain({ type: "mail_display_name", userId: syntheticUserId, adminSecret: syntheticAdminSecret, }); await Promise.resolve(); expect(fetchMock).toHaveBeenCalledOnce(); }); it("type=web → $fetch wird NICHT gerufen (web-Domains brauchen keinen Mail-Scan)", async () => { await triggerScanIfMailDomain({ type: "web", userId: syntheticUserId, adminSecret: syntheticAdminSecret, }); await Promise.resolve(); expect(fetchMock).not.toHaveBeenCalled(); }); it("Scan-Fehler blockiert POST-Response nicht — catch() schluckt Error", async () => { fetchMock = vi.fn().mockRejectedValue(new Error("IMAP timeout")); (globalThis as Record).$fetch = fetchMock; // Darf keinen unhandled rejection werfen await expect( triggerScanIfMailDomain({ type: "mail_domain", userId: syntheticUserId, adminSecret: syntheticAdminSecret, }), ).resolves.toBeUndefined(); await Promise.resolve(); // fetchMock wurde gerufen, Fehler wurde geschluckt expect(fetchMock).toHaveBeenCalledOnce(); }); it("adminSecret wird aus runtimeConfig übergeben — nicht hardcoded", async () => { const customSecret = "runtime-secret-abc123"; await triggerScanIfMailDomain({ type: "mail_domain", userId: syntheticUserId, adminSecret: customSecret, }); await Promise.resolve(); const [, callOpts] = fetchMock.mock.calls[0] as [string, { headers: Record }]; expect(callOpts.headers["x-admin-secret"]).toBe(customSecret); }); it("userId wird korrekt im Body übergeben", async () => { const specificUserId = "aaaabbbb-cccc-dddd-eeee-ffff00000002"; await triggerScanIfMailDomain({ type: "mail_domain", userId: specificUserId, adminSecret: syntheticAdminSecret, }); await Promise.resolve(); const [, callOpts] = fetchMock.mock.calls[0] as [string, { body: { userId: string } }]; expect(callOpts.body.userId).toBe(specificUserId); }); });