Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/emails/services/TransactionalEmailQueue.server.test.ts
T

148 lines
4.2 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from "vitest";
import { buildRetryJobOptions, buildTransactionalMailJob, queueNames, type TypedQueue } from "@toolctrl/queue";
import { queueTransactionalEmail } from "./TransactionalEmailQueue.server";
const runtimeQueue = {
add: vi.fn(async () => ({ id: "job_from_env" })),
close: vi.fn(async () => undefined),
};
const originalRedisEnv = {
BULLMQ_REDIS_URL: process.env.BULLMQ_REDIS_URL,
REDIS_URL: process.env.REDIS_URL,
};
function restoreRedisEnv() {
for (const [key, value] of Object.entries(originalRedisEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
vi.mock("@toolctrl/queue/runtime", () => ({
createQueue: vi.fn(() => runtimeQueue),
}));
afterEach(() => {
vi.clearAllMocks();
restoreRedisEnv();
});
describe("queueTransactionalEmail", () => {
test("fails closed without Redis configuration", async () => {
await expect(
queueTransactionalEmail(
{
to: "user@example.com",
subject: "Reset password",
body: "<p>Reset</p>",
alias: "password-reset",
},
{
env: {},
}
)
).rejects.toThrow("Redis connection required to enqueue transactional email.");
});
test("enqueues transactional email payloads", async () => {
const add = vi.fn(async () => ({ id: "job_1" }));
const queue = { add } as unknown as TypedQueue<typeof queueNames.email>;
const result = await queueTransactionalEmail(
{
to: "user@example.com",
subject: "Reset password",
body: "<p>Reset</p>",
alias: "password-reset",
tenantId: "tenant_1",
userId: "user_1",
data: { action_url: "https://example.com/reset" },
},
{
queue,
}
);
expect(result).toEqual({ enqueued: true, jobId: "job_1" });
expect(add).toHaveBeenCalledOnce();
const descriptor = buildTransactionalMailJob({
to: ["user@example.com"],
subject: "Reset password",
html: "<p>Reset</p>",
templateData: { action_url: "https://example.com/reset" },
tenantId: "tenant_1",
userId: "user_1",
metadata: { alias: "password-reset" },
});
expect(add).toHaveBeenCalledWith(
"send-transactional-email",
descriptor.payload,
expect.objectContaining({
...buildRetryJobOptions(descriptor),
// priority is resolved from tenantId; without a registered plan
// lookup it falls back to NORMAL (5).
priority: 5,
})
);
});
test("uses process Redis configuration by default", async () => {
process.env.BULLMQ_REDIS_URL = "redis://localhost:6379/2";
const result = await queueTransactionalEmail({
to: "user@example.com",
subject: "Reset password",
body: "<p>Reset</p>",
alias: "password-reset",
});
expect(result).toEqual({ enqueued: true, jobId: "job_from_env" });
expect(runtimeQueue.add).toHaveBeenCalledOnce();
expect(runtimeQueue.close).toHaveBeenCalledOnce();
});
test("propagates enqueue failures without synchronous mail fallback", async () => {
const enqueueError = new Error("queue unavailable");
const add = vi.fn(async () => {
throw enqueueError;
});
const queue = { add } as unknown as TypedQueue<typeof queueNames.email>;
await expect(
queueTransactionalEmail(
{
to: "user@example.com",
subject: "Welcome",
body: "<p>Welcome</p>",
alias: "registration-welcome",
data: { firstName: "Ada" },
},
{
queue,
}
)
).rejects.toThrow(enqueueError);
expect(add).toHaveBeenCalledOnce();
const descriptor = buildTransactionalMailJob({
to: ["user@example.com"],
subject: "Welcome",
html: "<p>Welcome</p>",
templateData: { firstName: "Ada" },
metadata: { alias: "registration-welcome" },
});
expect(add).toHaveBeenCalledWith(
"send-transactional-email",
expect.objectContaining({
kind: "mail.transactional",
templateData: { firstName: "Ada" },
metadata: { alias: "registration-welcome" },
}),
buildRetryJobOptions(descriptor)
);
});
});