Skip to content
File

Blob: tests/worker/services/inbox-cleanup.test.ts

typescript105 lines
1import { env } from "cloudflare:test";
2import { beforeEach, describe, expect, it, vi } from "vitest";
3import { createDb } from "@/worker/db";
4import { cleanupExpiredInboxes } from "@/worker/services/inbox";
5import { getRawStorageKey } from "@/worker/services/storage";
6import { resetWorkerState, seedDomain, seedEmail, seedInbox, seedSession } from "../api/helpers";
7 
8function getDb() {
9 return createDb(env.DB.withSession("first-primary"));
10}
11 
12describe("worker inbox cleanup service", () => {
13 beforeEach(async () => {
14 await resetWorkerState();
15 });
16 
17 it("deletes expired temporary inboxes in batches", async () => {
18 await seedDomain("mail.test", true);
19 
20 for (let index = 0; index < 101; index += 1) {
21 await seedInbox({
22 address: `user${index}@mail.test`,
23 createdAt: new Date("2026-01-01T00:00:00.000Z"),
24 expiresAt: new Date("2026-01-02T00:00:00.000Z"),
25 });
26 }
27 
28 const result = await cleanupExpiredInboxes(env);
29 const remaining = await getDb().query.inboxes.findMany();
30 
31 expect(result).toEqual({ deleted: 101 });
32 expect(remaining).toHaveLength(0);
33 });
34 
35 it("revokes session tokens and deletes storage before D1 rows", async () => {
36 await seedDomain("mail.test", true);
37 const inbox = await seedInbox({
38 address: "reader@mail.test",
39 createdAt: new Date("2026-01-01T00:00:00.000Z"),
40 expiresAt: new Date("2026-01-02T00:00:00.000Z"),
41 });
42 const token = await seedSession({
43 type: "user",
44 address: inbox.fullAddress,
45 });
46 const seededEmail = await seedEmail({
47 address: inbox.fullAddress,
48 inboxId: inbox.id,
49 attachments: [
50 {
51 filename: "invoice.txt",
52 contentType: "text/plain",
53 content: "attachment body",
54 },
55 ],
56 });
57 
58 const originalDelete = env.STORAGE.delete.bind(env.STORAGE);
59 let sawEmailRowDuringStorageDelete = false;
60 const deleteSpy = vi.spyOn(env.STORAGE, "delete").mockImplementation(async (keys) => {
61 const emailRow = await getDb().query.emails.findFirst({
62 where: (table, { eq }) => eq(table.id, seededEmail.emailId),
63 });
64 sawEmailRowDuringStorageDelete = emailRow !== undefined;
65 await originalDelete(keys);
66 });
67 
68 const result = await cleanupExpiredInboxes(env);
69 
70 deleteSpy.mockRestore();
71 
72 const storedInbox = await getDb().query.inboxes.findFirst({
73 where: (table, { eq }) => eq(table.id, inbox.id),
74 });
75 const storedEmail = await getDb().query.emails.findFirst({
76 where: (table, { eq }) => eq(table.id, seededEmail.emailId),
77 });
78 
79 expect(result).toEqual({ deleted: 1 });
80 expect(sawEmailRowDuringStorageDelete).toBe(true);
81 expect(storedInbox).toBeUndefined();
82 expect(storedEmail).toBeUndefined();
83 expect(await env.SESSIONS.get(`token:${token}`)).toBeNull();
84 expect(await env.SESSIONS.get(`inbox-token:${inbox.fullAddress}`)).toBeNull();
85 expect(await env.STORAGE.get(seededEmail.bodyKey)).toBeNull();
86 expect(await env.STORAGE.get(getRawStorageKey(seededEmail.emailId))).toBeNull();
87 expect(await env.STORAGE.get(seededEmail.attachments[0]?.storageKey ?? "missing")).toBeNull();
88 });
89 
90 it("handles the zero-expired-inbox case cleanly", async () => {
91 await seedDomain("mail.test", true);
92 const inbox = await seedInbox({
93 address: "reader@mail.test",
94 });
95 
96 const result = await cleanupExpiredInboxes(env);
97 const storedInbox = await getDb().query.inboxes.findFirst({
98 where: (table, { eq }) => eq(table.id, inbox.id),
99 });
100 
101 expect(result).toEqual({ deleted: 0 });
102 expect(storedInbox?.fullAddress).toBe(inbox.fullAddress);
103 });
104});