148 lines
4.2 KiB
TypeScript
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)
|
|
);
|
|
});
|
|
});
|