Files
mailflare/tests/permanent-delete.test.mjs
midego 2b7d78bac7 Auto-delete Trash and Spam after a chosen number of days
Adds Settings > Inbox > Trash and Spam clean-up (closes #68, part 2).

- users.trash_retention_days (null = never) via GET/PATCH /api/settings/trash-retention
- messages.trashed_at, kept current by triggers whichever code path changes status
- Scheduled maintenance purges expired messages in batches of 200 (Workers cron and Node scheduler)
2026-10-05 10:48:51 +07:00

89 lines
4.5 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import test, { after } from "node:test";
import { fileURLToPath, pathToFileURL } from "node:url";
import { build } from "esbuild";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const directory = mkdtempSync(join(root, "node_modules", "mailflare-permanent-delete-test-"));
after(() => rmSync(directory, { recursive: true, force: true }));
await build({
stdin: {
contents: `
export { isAllowedBulkMessageAction, getStatusForBulkAction, getReadValueForBulkAction, isPermanentlyDeletableStatus, isPermanentDeleteFolder } from "./src/app/api/messages/bulk/utils.ts";
export { supportsPermanentDelete, getEmptyFolderLabel, getPermanentDeleteConfirmText, getEmptyFolderConfirmText } from "./src/lib/messages/permanent-delete-utils.ts";
export { TRASH_RETENTION_OPTIONS, getTrashRetentionCutoff, normalizeTrashRetentionDays, describeTrashRetention } from "./src/lib/email/trash-retention-utils.ts";
export { updateTrashRetentionSettingsSchema } from "./src/lib/validators.ts";
`,
resolveDir: root,
sourcefile: "permanent-delete-test-entry.ts",
},
outfile: join(directory, "entry.mjs"),
bundle: true,
platform: "node",
format: "esm",
target: "node24",
tsconfig: join(root, "tsconfig.json"),
packages: "external",
logLevel: "silent",
});
const m = await import(pathToFileURL(join(directory, "entry.mjs")).href);
test("delete is an accepted bulk action that changes neither status nor read state", () => {
assert.equal(m.isAllowedBulkMessageAction("delete"), true);
assert.equal(m.getStatusForBulkAction("delete"), null);
assert.equal(m.getReadValueForBulkAction("delete"), null);
assert.equal(m.isAllowedBulkMessageAction("destroy"), false);
});
test("only Trash and Spam messages can be permanently deleted", () => {
for (const status of ["trash", "spam"]) assert.equal(m.isPermanentlyDeletableStatus(status), true);
for (const status of ["received", "sent", "draft", "archived", "", null, undefined]) {
assert.equal(m.isPermanentlyDeletableStatus(status), false, String(status));
}
assert.equal(m.isPermanentDeleteFolder("trash"), true);
assert.equal(m.isPermanentDeleteFolder("inbox"), false);
});
test("the UI offers permanent delete only in Trash and Spam", () => {
assert.equal(m.supportsPermanentDelete("trash"), true);
assert.equal(m.supportsPermanentDelete("spam"), true);
assert.equal(m.supportsPermanentDelete("inbox"), false);
assert.equal(m.supportsPermanentDelete(undefined), false);
assert.equal(m.getEmptyFolderLabel("trash"), "Empty Trash");
assert.equal(m.getEmptyFolderLabel("spam"), "Empty Spam");
});
test("confirmation text says how much is lost and that it is irreversible", () => {
assert.equal(m.getPermanentDeleteConfirmText(1), "Permanently delete this message? This cannot be undone.");
assert.equal(m.getPermanentDeleteConfirmText(3), "Permanently delete these 3 messages? This cannot be undone.");
assert.equal(m.getEmptyFolderConfirmText("trash", 12), "Permanently delete all 12 messages in Trash? This cannot be undone.");
assert.equal(m.getEmptyFolderConfirmText("spam", 1), "Permanently delete the 1 message in Spam? This cannot be undone.");
assert.equal(m.getEmptyFolderConfirmText("trash"), "Permanently delete every message in Trash? This cannot be undone.");
});
test("retention cutoff is the given number of days before now", () => {
const now = new Date("2026-10-31T12:00:00Z");
assert.equal(m.getTrashRetentionCutoff(now, 30).toISOString(), "2026-10-01T12:00:00.000Z");
assert.equal(m.getTrashRetentionCutoff(now, 1).toISOString(), "2026-10-30T12:00:00.000Z");
});
test("retention days outside 1-365 or non-integers mean never", () => {
for (const days of m.TRASH_RETENTION_OPTIONS) assert.equal(m.normalizeTrashRetentionDays(days), days);
for (const days of [null, undefined, 0, -3, 366, 2.5, "30"]) assert.equal(m.normalizeTrashRetentionDays(days), null, String(days));
assert.equal(m.describeTrashRetention(null), "Never");
assert.equal(m.describeTrashRetention(1), "After 1 day");
assert.equal(m.describeTrashRetention(30), "After 30 days");
});
test("the settings API accepts whole days from 1 to 365, or null to turn it off", () => {
const ok = (days) => m.updateTrashRetentionSettingsSchema.safeParse({ days }).success;
assert.equal(ok(30), true);
assert.equal(ok(null), true);
assert.equal(ok(0), false);
assert.equal(ok(400), false);
assert.equal(ok(1.5), false);
assert.equal(ok("30"), false);
});