Skip to content
File

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

typescript266 lines
1import { env } from "cloudflare:test";
2import { beforeEach, describe, expect, it } from "vitest";
3import { createDb } from "@/worker/db";
4import {
5 addDomain,
6 createTemporaryInbox,
7 deleteDomainByName,
8 deleteInbox,
9 extendTemporaryInbox,
10 updateDomainStatus,
11} from "@/worker/services/inbox";
12import { PublicError } from "@/worker/security";
13import { resetWorkerState, seedDomain, seedEmail, seedInbox, seedSession } from "../api/helpers";
14 
15function getDb() {
16 return createDb(env.DB.withSession("first-primary"));
17}
18 
19describe("worker inbox lifecycle services", () => {
20 beforeEach(async () => {
21 await resetWorkerState();
22 });
23 
24 it("creates a temporary inbox on an active domain", async () => {
25 await seedDomain("mail.test", true);
26 
27 const result = await createTemporaryInbox(env, "mail.test", 48, getDb());
28 
29 expect(result.address).toMatch(/@mail\.test$/);
30 expect(result.ttlHours).toBe(48);
31 expect(result.token).toMatch(/^tok_/);
32 
33 const storedInbox = await getDb().query.inboxes.findFirst({
34 where: (table, { eq }) => eq(table.fullAddress, result.address),
35 });
36 
37 expect(storedInbox?.isPermanent).toBe(false);
38 expect(storedInbox?.expiresAt?.toISOString()).toBe(result.expiresAt.toISOString());
39 });
40 
41 it("rejects inbox creation for inactive or missing domains", async () => {
42 await seedDomain("inactive.test", false);
43 
44 await expect(createTemporaryInbox(env, "inactive.test", 24, getDb())).rejects.toThrowError(PublicError);
45 await expect(createTemporaryInbox(env, "missing.test", 24, getDb())).rejects.toThrow(
46 "Requested domain is not available",
47 );
48 });
49 
50 it("extends a temporary inbox forward only", async () => {
51 await seedDomain("mail.test", true);
52 const inbox = await seedInbox({
53 address: "reader@mail.test",
54 createdAt: new Date("2026-04-15T00:00:00.000Z"),
55 expiresAt: new Date("2026-04-16T00:00:00.000Z"),
56 });
57 const token = await seedSession({
58 type: "user",
59 address: inbox.fullAddress,
60 });
61 
62 const result = await extendTemporaryInbox(
63 env,
64 inbox,
65 token,
66 {
67 type: "user",
68 address: inbox.fullAddress,
69 },
70 72,
71 getDb(),
72 );
73 
74 expect(result).toEqual({
75 expiresAt: new Date("2026-04-18T00:00:00.000Z"),
76 ttlHours: 72,
77 });
78 
79 const storedInbox = await getDb().query.inboxes.findFirst({
80 where: (table, { eq }) => eq(table.id, inbox.id),
81 });
82 
83 expect(storedInbox?.expiresAt?.toISOString()).toBe("2026-04-18T00:00:00.000Z");
84 });
85 
86 it("rejects extension for permanent inboxes or non-forward ttl values", async () => {
87 const permanentInbox = await seedInbox({
88 address: "admin@mail.test",
89 isPermanent: true,
90 });
91 const temporaryInbox = await seedInbox({
92 address: "reader@mail.test",
93 createdAt: new Date("2026-04-15T00:00:00.000Z"),
94 expiresAt: new Date("2026-04-17T00:00:00.000Z"),
95 });
96 
97 await expect(
98 extendTemporaryInbox(
99 env,
100 permanentInbox,
101 "tok_admin",
102 { type: "admin", sub: "00000000-0000-4000-8000-000000000001" },
103 72,
104 getDb(),
105 ),
106 ).rejects.toThrow("Permanent inboxes cannot be extended");
107 
108 await expect(
109 extendTemporaryInbox(
110 env,
111 temporaryInbox,
112 "tok_user",
113 { type: "user", address: temporaryInbox.fullAddress },
114 48,
115 getDb(),
116 ),
117 ).rejects.toThrow("Inbox already extends beyond the requested duration");
118 });
119 
120 it("deletes temporary inboxes and revokes user sessions", async () => {
121 await seedDomain("mail.test", true);
122 const inbox = await seedInbox({
123 address: "reader@mail.test",
124 });
125 const token = await seedSession({
126 type: "user",
127 address: inbox.fullAddress,
128 });
129 await seedEmail({
130 address: inbox.fullAddress,
131 inboxId: inbox.id,
132 attachments: [
133 {
134 filename: "invoice.txt",
135 contentType: "text/plain",
136 content: "attachment body",
137 },
138 ],
139 });
140 
141 await deleteInbox(env, inbox, getDb());
142 
143 const storedInbox = await getDb().query.inboxes.findFirst({
144 where: (table, { eq }) => eq(table.id, inbox.id),
145 });
146 
147 expect(storedInbox).toBeUndefined();
148 expect(await env.SESSIONS.get(`token:${token}`)).toBeNull();
149 expect(await env.SESSIONS.get(`inbox-token:${inbox.fullAddress}`)).toBeNull();
150 });
151 
152 it("rejects delete for permanent inboxes", async () => {
153 const inbox = await seedInbox({
154 address: "admin@mail.test",
155 isPermanent: true,
156 });
157 
158 await expect(deleteInbox(env, inbox, getDb())).rejects.toThrow("Permanent inboxes cannot be deleted");
159 });
160 
161 it("adds a domain and seeds reserved permanent inboxes when active", async () => {
162 await addDomain(env, "NewDomain.TEST", true, getDb());
163 
164 const storedDomain = await getDb().query.domains.findFirst({
165 where: (table, { eq }) => eq(table.domain, "newdomain.test"),
166 });
167 const domainInboxes = await getDb().query.inboxes.findMany({
168 where: (table, { eq }) => eq(table.domain, "newdomain.test"),
169 });
170 
171 expect(storedDomain?.isActive).toBe(true);
172 expect(domainInboxes.map((item) => item.localPart).sort()).toEqual(["abuse", "admin", "postmaster", "webmaster"]);
173 });
174 
175 it("does not duplicate reserved inboxes when re-enabling a domain", async () => {
176 await seedDomain("mail.test", false);
177 
178 await updateDomainStatus(env, "mail.test", true, getDb());
179 await updateDomainStatus(env, "mail.test", false, getDb());
180 await updateDomainStatus(env, "mail.test", true, getDb());
181 
182 const domainInboxes = await getDb().query.inboxes.findMany({
183 where: (table, { eq }) => eq(table.domain, "mail.test"),
184 });
185 
186 expect(domainInboxes).toHaveLength(4);
187 });
188 
189 it("disables a domain without deleting existing inboxes", async () => {
190 await seedDomain("mail.test", true);
191 await seedInbox({
192 address: "reader@mail.test",
193 });
194 
195 await updateDomainStatus(env, "mail.test", false, getDb());
196 
197 const storedDomain = await getDb().query.domains.findFirst({
198 where: (table, { eq }) => eq(table.domain, "mail.test"),
199 });
200 const storedInbox = await getDb().query.inboxes.findFirst({
201 where: (table, { eq }) => eq(table.fullAddress, "reader@mail.test"),
202 });
203 
204 expect(storedDomain?.isActive).toBe(false);
205 expect(storedInbox?.fullAddress).toBe("reader@mail.test");
206 });
207 
208 it("refuses to delete domains that still have non-deletable inboxes or reserved inbox email", async () => {
209 await seedDomain("mail.test", true);
210 await seedInbox({
211 address: "reader@mail.test",
212 });
213 
214 await expect(deleteDomainByName(env, "mail.test", getDb())).rejects.toThrow(
215 "Domain still has inboxes. Disable it instead of deleting it.",
216 );
217 
218 await resetWorkerState();
219 await seedDomain("mail.test", true);
220 const reservedInbox = await seedInbox({
221 address: "admin@mail.test",
222 isPermanent: true,
223 });
224 await seedEmail({
225 address: reservedInbox.fullAddress,
226 inboxId: reservedInbox.id,
227 });
228 
229 await expect(deleteDomainByName(env, "mail.test", getDb())).rejects.toThrow(
230 "Domain still has inboxes. Disable it instead of deleting it.",
231 );
232 });
233 
234 it("allows domain delete when only reserved inboxes remain with zero emails", async () => {
235 await seedDomain("mail.test", true);
236 await seedInbox({
237 address: "admin@mail.test",
238 isPermanent: true,
239 });
240 await seedInbox({
241 address: "postmaster@mail.test",
242 isPermanent: true,
243 });
244 await seedInbox({
245 address: "abuse@mail.test",
246 isPermanent: true,
247 });
248 await seedInbox({
249 address: "webmaster@mail.test",
250 isPermanent: true,
251 });
252 
253 await deleteDomainByName(env, "mail.test", getDb());
254 
255 const storedDomain = await getDb().query.domains.findFirst({
256 where: (table, { eq }) => eq(table.domain, "mail.test"),
257 });
258 const remainingInboxes = await getDb().query.inboxes.findMany({
259 where: (table, { eq }) => eq(table.domain, "mail.test"),
260 });
261 
262 expect(storedDomain).toBeUndefined();
263 expect(remainingInboxes).toHaveLength(0);
264 });
265});