统一 Node 22 与后端安全运行基线(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:17:18 +08:00
父节点 dcf34d7ac6
当前提交 abb6a22f5c
共修改 28 个文件,包含 2250 行新增和 3012 行删除
@@ -0,0 +1,90 @@
const { initTracing, shutdownTracing } = require("../../../src/observability/tracing");
function buildDependencies({ constructError = null } = {}) {
const instances = [];
const resourceFromAttributes = vi.fn((attributes) => ({ attributes }));
const getNodeAutoInstrumentations = vi.fn((options) => ({ options }));
const logger = { log: vi.fn(), error: vi.fn() };
class TestNodeSDK {
constructor(options) {
if (constructError) {
throw constructError;
}
this.options = options;
this.start = vi.fn();
this.shutdown = vi.fn(async () => {});
instances.push(this);
}
}
return {
overrides: {
NodeSDK: TestNodeSDK,
ConsoleSpanExporter: class TestConsoleSpanExporter {},
resourceFromAttributes,
getNodeAutoInstrumentations,
logger,
},
instances,
resourceFromAttributes,
getNodeAutoInstrumentations,
logger,
};
}
describe("observability tracing", () => {
afterEach(async () => {
await shutdownTracing();
});
it("does not initialize the SDK when traces are disabled", () => {
const dependencies = buildDependencies();
expect(initTracing({ otelTracesEnabled: false }, dependencies.overrides)).toBeNull();
expect(dependencies.instances).toHaveLength(0);
});
it("initializes the SDK with the service resource and filesystem instrumentation disabled", () => {
const dependencies = buildDependencies();
const result = initTracing({ otelTracesEnabled: true }, dependencies.overrides);
expect(result).toBe(dependencies.instances[0]);
expect(dependencies.resourceFromAttributes).toHaveBeenCalledWith({ "service.name": "hyb-oa-server" });
expect(dependencies.getNodeAutoInstrumentations).toHaveBeenCalledWith({
"@opentelemetry/instrumentation-fs": { enabled: false },
});
expect(result.start).toHaveBeenCalledOnce();
expect(dependencies.logger.log).toHaveBeenCalledWith("OTel tracing initialized");
});
it("supports a distinct worker service name", () => {
const dependencies = buildDependencies();
initTracing({ otelTracesEnabled: true }, {
...dependencies.overrides,
serviceName: "hyb-oa-worker",
});
expect(dependencies.resourceFromAttributes).toHaveBeenCalledWith({ "service.name": "hyb-oa-worker" });
});
it("contains initialization failures and leaves tracing disabled", () => {
const error = new Error("otel_init_failed");
const dependencies = buildDependencies({ constructError: error });
expect(initTracing({ otelTracesEnabled: true }, dependencies.overrides)).toBeNull();
expect(dependencies.logger.error).toHaveBeenCalledWith("Error initializing OTel tracing", error);
});
it("shuts down an initialized SDK once", async () => {
const dependencies = buildDependencies();
const instance = initTracing({ otelTracesEnabled: true }, dependencies.overrides);
await shutdownTracing();
await shutdownTracing();
expect(instance.shutdown).toHaveBeenCalledOnce();
});
});
+11 -5
查看文件
@@ -21,6 +21,11 @@ const INACTIVE_EMPLOYEE_ID = "00000000-0000-4000-8000-000000000004";
const POLICY_ID = "10000000-0000-4000-8000-000000000001";
const GENERAL_ID = "10000000-0000-4000-8000-000000000002";
const HIDDEN_ID = "10000000-0000-4000-8000-000000000003";
const TEST_FUTURE_OFFSET_MS = 24 * 60 * 60 * 1000;
function buildFutureDate() {
return new Date(Date.now() + TEST_FUTURE_OFFSET_MS);
}
function buildEmployee(overrides = {}) {
return {
@@ -47,7 +52,7 @@ function buildAnnouncement(overrides = {}) {
category: "policy",
requires_acknowledgement: true,
acknowledgement_statement: "我已阅读并理解该制度。",
acknowledgement_due_at: new Date("2026-06-01T15:59:59.000Z"),
acknowledgement_due_at: buildFutureDate(),
created_at: now,
created_by: ADMIN_ID,
updated_at: now,
@@ -76,11 +81,12 @@ function matchAnnouncementWhere(item, where = {}) {
if (where.enabled !== undefined && item.enabled !== where.enabled) {
return false;
}
const now = new Date("2026-05-28T02:00:00.000Z");
if (where.OR && item.starts_at && item.starts_at > now) {
const startsAtUpperBound = where.OR?.find((clause) => clause.starts_at?.lte)?.starts_at.lte ?? new Date();
const endsAtLowerBound = where.AND?.[0]?.OR?.find((clause) => clause.ends_at?.gte)?.ends_at.gte ?? new Date();
if (where.OR && item.starts_at && item.starts_at > startsAtUpperBound) {
return false;
}
if (where.AND && item.ends_at && item.ends_at < now) {
if (where.AND && item.ends_at && item.ends_at < endsAtLowerBound) {
return false;
}
return true;
@@ -107,7 +113,7 @@ function createPrismaState(options = {}) {
buildAnnouncement({
id: HIDDEN_ID,
title: "未来公告",
starts_at: new Date("2026-06-01T00:00:00.000Z"),
starts_at: buildFutureDate(),
}),
];
const acknowledgements = options.acknowledgements ?? [];
@@ -0,0 +1,141 @@
const { validateDevSeedSafety } = require("../../../src/scripts/dev-seed-safety");
const { ensureEmployee } = require("../../../src/scripts/seed-dev");
function safeEnv(overrides = {}) {
return {
NODE_ENV: "development",
SEED_DEV_ALLOW: "1",
SEED_DEV_DATABASE_CONFIRM: "hyb_oa_test",
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test?schema=public",
...overrides,
};
}
function employeePayload() {
return {
employeeNo: "DEV001",
name: "开发员工",
phone: "00000000000",
email: "[email protected]",
password: "OneTimePassword1234",
departmentId: "department-1",
legalEntityId: "entity-1",
title: "测试",
directManagerId: null,
roles: ["employee"],
hireDate: new Date("2020-01-01T00:00:00.000Z"),
};
}
function buildPrisma(existing = { id: "employee-1" }) {
return {
employee: {
findFirst: vi.fn(async () => existing),
update: vi.fn(async () => ({ id: existing?.id ?? "employee-1" })),
create: vi.fn(async () => ({ id: "employee-created" })),
},
employeeRole: {
deleteMany: vi.fn(async () => ({ count: 0 })),
createMany: vi.fn(async () => ({ count: 1 })),
},
};
}
describe("dev seed safety guard", () => {
it("rejects production even when all explicit switches are present", () => {
expect(() => validateDevSeedSafety(safeEnv({ NODE_ENV: "production" }))).toThrowError(
expect.objectContaining({ code: "production_forbidden" }),
);
});
it("rejects execution without the explicit allow switch", () => {
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_ALLOW: "" }))).toThrowError(
expect.objectContaining({ code: "explicit_allow_required" }),
);
});
it("rejects non-loopback database targets", () => {
expect(() => validateDevSeedSafety(safeEnv({
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test",
}))).toThrowError(expect.objectContaining({ code: "database_host_not_local" }));
});
it("rejects a query-string host override that would bypass the loopback authority", () => {
expect(() => validateDevSeedSafety(safeEnv({
DATABASE_URL: "postgresql://local-user:[email protected]:5432/hyb_oa_test?host=database.internal",
}))).toThrowError(expect.objectContaining({ code: "database_host_not_local" }));
});
it("rejects a missing or mismatched database confirmation", () => {
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_DATABASE_CONFIRM: "" }))).toThrowError(
expect.objectContaining({ code: "database_confirmation_mismatch" }),
);
expect(() => validateDevSeedSafety(safeEnv({ SEED_DEV_DATABASE_CONFIRM: "another_database" }))).toThrowError(
expect.objectContaining({ code: "database_confirmation_mismatch" }),
);
});
it.each(["localhost", "127.0.0.1", "[::1]"])("accepts the local host %s with exact confirmation", (host) => {
expect(validateDevSeedSafety(safeEnv({
DATABASE_URL: `postgresql://local-user:local-password@${host}:5432/hyb_oa_test`,
}))).toEqual({ databaseName: "hyb_oa_test", resetPasswords: false });
});
it("enables one-time password rotation only through the separate reset switch", () => {
expect(validateDevSeedSafety(safeEnv({ SEED_DEV_RESET_PASSWORDS: "1" }))).toEqual({
databaseName: "hyb_oa_test",
resetPasswords: true,
});
});
it("does not include database credentials or target details in refusal messages", () => {
const env = safeEnv({
DATABASE_URL: "postgresql://sensitive-user:[email protected]:5432/sensitive_database",
SEED_DEV_DATABASE_CONFIRM: "sensitive_database",
});
try {
validateDevSeedSafety(env);
throw new Error("expected guard refusal");
} catch (error) {
expect(error.message).not.toMatch(/super-secret|sensitive-user|database\.internal|sensitive_database/);
}
});
});
describe("dev seed employee password behavior", () => {
it("preserves the password fields of an existing account by default", async () => {
const prisma = buildPrisma();
const result = await ensureEmployee(prisma, employeePayload());
const updateData = prisma.employee.update.mock.calls[0][0].data;
expect(updateData).not.toHaveProperty("password_hash");
expect(updateData).not.toHaveProperty("must_change_password");
expect(result.seedPasswordAction).toBe("preserved");
});
it("rotates an existing password only when requested and marks it one-time", async () => {
const prisma = buildPrisma();
const result = await ensureEmployee(prisma, employeePayload(), { resetPasswords: true });
const updateData = prisma.employee.update.mock.calls[0][0].data;
expect(updateData.password_hash).toEqual(expect.any(String));
expect(updateData.password_hash).not.toContain("OneTimePassword1234");
expect(updateData.must_change_password).toBe(true);
expect(result.seedPasswordAction).toBe("reset");
});
it("creates a missing development account without printing its credential", async () => {
const prisma = buildPrisma(null);
const result = await ensureEmployee(prisma, employeePayload());
const createData = prisma.employee.create.mock.calls[0][0].data;
expect(createData.password_hash).toEqual(expect.any(String));
expect(createData.password_hash).not.toContain("OneTimePassword1234");
expect(createData.must_change_password).toBe(true);
expect(result.seedPasswordAction).toBe("created");
});
});
+126 -1
查看文件
@@ -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]");
});
});
@@ -114,7 +114,7 @@ describe("workspace small-team navigation defaults", () => {
});
const mainLabels = navigation.sections.main.map((item) => item.label);
expect(mainLabels).toEqual(["工作台", "报销", "采购申请", "项目", "资产", "请假记录"]);
expect(mainLabels).toEqual(["工作台", "报销", "采购", "项目", "资产", "请假记录"]);
expect(mainLabels).not.toContain("待我处理");
expect(mainLabels).not.toContain("票据");
expect(mainLabels).not.toContain("个人设置");
@@ -150,8 +150,8 @@ describe("workspace small-team navigation defaults", () => {
"工作台",
"待我处理",
"报销",
"采购申请",
"合同台账",
"采购",
"合同",
"项目",
"资产",
"团队",