Fix 1 (scan-internal): Gmail ignoriert IMAP EXPUNGE — stattdessen messageMove() in Trash-Folder (via specialUse='\\Trash', Fallback '[Gmail]/Trash'). Verhindert dass Gambling-Mails bei Gmail-Usern in 'All Mail' verbleiben statt zu verschwinden. Alle anderen Provider (iCloud, Outlook, IONOS) bleiben beim bestehenden messageDelete() + EXPUNGE-Fallback. Fix 2 (custom-domains): Nach erfolgreichem mail_domain-Add fire-and-forget $fetch auf /api/mail/scan-internal — damit neue Mail-Patterns sofort (< 5s) wirken statt erst beim nächsten 30min-Cron. Scan-Fehler blockieren den POST nicht. Tests: 16 neue Tests (gmail-delete-strategy + scan-trigger). 259 passed, 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
5.8 KiB
TypeScript
173 lines
5.8 KiB
TypeScript
/**
|
|
* 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<typeof vi.fn>;
|
|
|
|
function setupFetchMock(resolveWith: unknown = { ok: true }) {
|
|
fetchMock = vi.fn().mockResolvedValue(resolveWith);
|
|
(globalThis as Record<string, unknown>).$fetch = fetchMock;
|
|
}
|
|
|
|
function teardownFetchMock() {
|
|
delete (globalThis as Record<string, unknown>).$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<void> {
|
|
const { type, userId, adminSecret } = opts;
|
|
|
|
if (type === "mail_domain" || type === "mail_display_name") {
|
|
const globalFetch = (globalThis as Record<string, unknown>).$fetch as (
|
|
url: string,
|
|
opts: unknown,
|
|
) => Promise<unknown>;
|
|
|
|
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<string, unknown>).$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<string, string> }];
|
|
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);
|
|
});
|
|
});
|