1
25import { afterAll, beforeAll, describe, expect, it } from "vitest";
26import { Queue } from "bullmq";
27import Redis from "ioredis";
28import { createTestApp, type TestAppHandle } from "./helpers/api.js";
29import { getRedisUrl } from "./helpers/containers.js";
30import {
31 buildInstantEmailData,
32 handleNotificationCreated
33} from "../../apps/worker/src/processors/email-decision.processor.js";
34import { createEmailWorker } from "../../apps/worker/src/processors/email.processor.js";
35import { QUEUES, enqueue } from "../../apps/worker/src/queues.js";
36import type { WorkerEnv } from "../../apps/worker/src/config.js";
37import type { PrismaClient } from "@prisma/client";
38
39describe("instant-notification email jobId (real BullMQ)", () => {
40 let app: TestAppHandle;
41 let redis: Redis;
42 let q: Queue;
43 let worker: ReturnType<typeof createEmailWorker>;
44 let prisma: PrismaClient;
45 let notifId: string;
46 let sent: { to: string; subject: string } | null;
47
48 beforeAll(async () => {
49 app = await createTestApp();
50 prisma = app.prisma;
51 redis = new Redis(await getRedisUrl(), { maxRetriesPerRequest: null });
52 q = new Queue("email", { connection: redis });
53
54
55
56
57
58 sent = null;
59 const env = {
60 SMTP_HOST: "smtp.fake",
61 SMTP_PORT: 587,
62 SMTP_FROM: "noreply@factory.test",
63 SMTP_STARTTLS: false
64 } as unknown as WorkerEnv;
65 worker = createEmailWorker({
66 connection: redis,
67 prisma,
68 env,
69 concurrency: 1,
70 smtp: async (o) => {
71 sent = { to: String(o.to[0]), subject: o.subject };
72 }
73 });
74 await worker.pause();
75 }, 180_000);
76
77 afterAll(async () => {
78 await worker?.close();
79 await q?.close();
80 await redis?.quit().catch(() => {});
81 await app?.close();
82 }, 30_000);
83
84 it("real enqueue does not throw + dedup on redelivery + worker stamps emailSentAt", async () => {
85 const run = Math.random().toString(36).slice(2, 8);
86 const user = await prisma.user.create({
87 data: { email: `jobid-probe-${run}@example.com`, name: "JobId Probe" }
88 });
89 const actor = await prisma.user.create({
90 data: { email: `jobid-actor-${run}@example.com`, name: "JobId Actor" }
91 });
92 const ws = await prisma.workspace.create({
93 data: { name: `JobId WS ${run}`, slug: `jobid-${run}` }
94 });
95 const notif = await prisma.notification.create({
96 data: {
97 workspaceId: ws.id,
98 userId: user.id,
99 actorId: actor.id,
100 type: "MENTION",
101 title: "JobId probe mention"
102 }
103 });
104 notifId = notif.id;
105
106
107
108
109 const res = await handleNotificationCreated(
110 prisma,
111 redis,
112 { PUBLIC_URL: "http://factory.test" },
113 {
114 workspaceId: ws.id,
115 notificationId: notif.id,
116 userId: user.id,
117 type: "MENTION",
118 title: "JobId probe mention",
119 deepLink: { type: "page", id: "p-nonexistent" },
120 actorName: "JobId Actor"
121 }
122 );
123 expect(res.action).toBe("instant");
124
125 const job = await q.getJob(`email_${notif.id}`);
126 expect(job).toBeTruthy();
127
128
129
130 const data = buildInstantEmailData({
131 to: user.email,
132 workspaceName: ws.name,
133 title: "JobId probe mention",
134 actorName: "JobId Actor",
135 link: { type: "page", id: "p-nonexistent" },
136 url: "http://factory.test/w/jobid/page/p-nonexistent",
137 notificationId: notif.id
138 });
139 const re = await enqueue(redis, QUEUES.email, data, { jobId: `email_${notif.id}` });
140 expect(re.id).toBe(`email_${notif.id}`);
141 const waiting = await q.getJobs(["waiting"], 0, -1);
142 expect(waiting.filter((j) => j.id === `email_${notif.id}`)).toHaveLength(1);
143
144
145
146
147
148 const finished: Promise<unknown> = new Promise((resolve, reject) => {
149 const t = setTimeout(
150 () => reject(new Error("email job did not complete in 15 s")),
151 15_000
152 );
153 worker.once("failed", (j, err) => {
154 clearTimeout(t);
155 reject(new Error(`email job FAILED: ${err?.message ?? "?"}`));
156 });
157 worker.once("completed", (j) => {
158 clearTimeout(t);
159 resolve(j);
160 });
161 });
162 await worker.resume();
163 const done = (await finished) as { id: string };
164 expect(done.id).toBe(`email_${notif.id}`);
165 expect(sent?.to).toBe(user.email);
166 expect(sent?.subject).toBeTruthy();
167 const stamped = await prisma.notification.findUniqueOrThrow({
168 where: { id: notif.id },
169 select: { emailSentAt: true }
170 });
171 expect(stamped.emailSentAt).toBeInstanceOf(Date);
172
173
174 await prisma.notification.deleteMany({ where: { id: notif.id } });
175 await prisma.workspace.deleteMany({ where: { id: ws.id } });
176 await prisma.user.deleteMany({ where: { id: { in: [user.id, actor.id] } } });
177 }, 30_000);
178});
179