统一 Node 22 与后端安全运行基线(07-15-production-trust-closeout-cleanup)
这个提交包含在:
@@ -1,4 +1,22 @@
|
||||
const { resolveEmailDeliveryStatus } = require("../../../src/services/email");
|
||||
const {
|
||||
resolveEmailDeliveryStatus,
|
||||
sendNotificationEmail,
|
||||
} = require("../../../src/services/email");
|
||||
|
||||
function buildEmailConfig(overrides = {}) {
|
||||
return {
|
||||
emailEnabled: true,
|
||||
emailFrom: "[email protected]",
|
||||
emailBaseUrl: "https://oa.hyinda.test/",
|
||||
appOrigin: "",
|
||||
smtpHost: "smtp.hyinda.test",
|
||||
smtpPort: 587,
|
||||
smtpSecure: false,
|
||||
smtpUser: "sender",
|
||||
smtpPass: "test-only-password",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("email delivery status", () => {
|
||||
it("reports ready only when SMTP delivery has the required public settings", () => {
|
||||
@@ -35,3 +53,110 @@ describe("email delivery status", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification email delivery", () => {
|
||||
it("skips delivery without creating a transporter when email is disabled", async () => {
|
||||
const createTransport = vi.fn();
|
||||
const prisma = { employee: { findMany: vi.fn() } };
|
||||
|
||||
const result = await sendNotificationEmail({
|
||||
prisma,
|
||||
config: buildEmailConfig({ emailEnabled: false }),
|
||||
recipientEmployeeIds: ["employee-1"],
|
||||
subject: "待办提醒",
|
||||
body: "请处理待办。",
|
||||
createTransport,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ skipped: true, sent: 0, failed: 0 });
|
||||
expect(createTransport).not.toHaveBeenCalled();
|
||||
expect(prisma.employee.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a Nodemailer 9 transporter and sends text mail with a safe link", async () => {
|
||||
const sendMail = vi.fn(async () => ({ messageId: "message-1" }));
|
||||
const createTransport = vi.fn(() => ({ sendMail }));
|
||||
const prisma = {
|
||||
employee: {
|
||||
findMany: vi.fn(async () => [
|
||||
{ id: "employee-1", email: "[email protected]", name: "员工一" },
|
||||
{ id: "employee-2", email: null, name: "员工二" },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await sendNotificationEmail({
|
||||
prisma,
|
||||
config: buildEmailConfig({ smtpHost: "smtp-success.hyinda.test" }),
|
||||
recipientEmployeeIds: ["employee-1", "employee-2", "employee-1"],
|
||||
subject: "待办提醒",
|
||||
body: "请处理待办。",
|
||||
type: "workflow_task",
|
||||
targetPath: "/workflows/tasks/1",
|
||||
requestId: "request-1",
|
||||
createTransport,
|
||||
});
|
||||
|
||||
expect(createTransport).toHaveBeenCalledWith({
|
||||
host: "smtp-success.hyinda.test",
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: { user: "sender", pass: "test-only-password" },
|
||||
});
|
||||
expect(sendMail).toHaveBeenCalledWith({
|
||||
from: "[email protected]",
|
||||
to: "[email protected]",
|
||||
subject: "待办提醒",
|
||||
text: "请处理待办。\n\n查看详情:https://oa.hyinda.test/workflows/tasks/1",
|
||||
headers: {
|
||||
"x-hyb-notification-type": "workflow_task",
|
||||
"x-request-id": "request-1",
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({ skipped: false, sent: 1, failed: 1 });
|
||||
});
|
||||
|
||||
it("counts send failures without logging the recipient email address", async () => {
|
||||
const error = new Error("smtp_rejected");
|
||||
error.code = "EENVELOPE";
|
||||
error.command = "RCPT TO";
|
||||
error.responseCode = 550;
|
||||
error.response = "550 rejected [email protected]";
|
||||
error.rejected = ["[email protected]"];
|
||||
const createTransport = vi.fn(() => ({ sendMail: vi.fn(async () => { throw error; }) }));
|
||||
const logger = { error: vi.fn() };
|
||||
const prisma = {
|
||||
employee: {
|
||||
findMany: vi.fn(async () => [
|
||||
{ id: "employee-3", email: "[email protected]", name: "员工三" },
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await sendNotificationEmail({
|
||||
prisma,
|
||||
config: buildEmailConfig({ smtpHost: "smtp-failure.hyinda.test" }),
|
||||
logger,
|
||||
requestId: "request-2",
|
||||
recipientEmployeeIds: ["employee-3"],
|
||||
subject: "待办提醒",
|
||||
body: "请处理待办。",
|
||||
createTransport,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ skipped: false, sent: 0, failed: 1 });
|
||||
expect(logger.error).toHaveBeenCalledOnce();
|
||||
const [context] = logger.error.mock.calls[0];
|
||||
expect(context).toMatchObject({
|
||||
errorCode: "email_delivery_failed",
|
||||
providerCode: "EENVELOPE",
|
||||
smtpCommand: "RCPT TO",
|
||||
smtpResponseCode: 550,
|
||||
requestId: "request-2",
|
||||
employeeId: "employee-3",
|
||||
});
|
||||
expect(context).not.toHaveProperty("err");
|
||||
expect(context).not.toHaveProperty("email");
|
||||
expect(JSON.stringify(context)).not.toContain("[email protected]");
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户