完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+80 -11
查看文件
@@ -55,6 +55,12 @@ async function buildXlsxBuffer(rows) {
async function buildApp(options = {}) {
const app = Fastify();
const actor = options.actor ?? {
id: "actor-1",
department_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
roleCodes: ["employee", "system_admin"],
permissionCodes: ["system.accounts.manage"],
};
const existingEmployees = options.existingEmployees ?? [];
const departments = options.departments ?? [];
const auditCreate = vi.fn().mockResolvedValue({});
@@ -100,24 +106,19 @@ async function buildApp(options = {}) {
$transaction: vi.fn(async (callback) => callback(tx)),
});
app.decorate("requirePermission", (permissionCode) => async (request) => {
request.employee = {
id: "actor-1",
department_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
permissionCodes: ["system.users"],
};
request.employee = actor;
const permissions = request.employee.permissionCodes ?? [];
if (!permissions.includes("*") && !permissions.includes(permissionCode)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
}
});
app.decorate("authenticate", async (request) => {
request.employee = {
id: "actor-1",
department_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
permissionCodes: ["system.users"],
};
request.employee = actor;
});
app.decorate("hasPermission", (employee, permissionCode) => {
const permissions = employee?.permissionCodes ?? [];
return permissions.includes("*") || permissions.includes(permissionCode);
});
app.decorate("hasPermission", () => true);
await app.register(multipart, {
attachFieldsToBody: false,
@@ -137,6 +138,35 @@ async function buildApp(options = {}) {
}
describe("users import routes", () => {
it("rejects the legacy system.users permission before reading the import", async () => {
const { app, auditCreate, departmentFindMany, employeeFindMany } = await buildApp({
actor: {
id: "actor-1",
department_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
roleCodes: ["employee"],
permissionCodes: ["system.users"],
},
});
const multipartRequest = buildMultipartRequest({
filename: "users.csv",
contentType: "text/csv",
content: "employeeNo,name,phone,email,departmentName\nE001,Alice,13800000000,[email protected],行政部\n",
});
const response = await app.inject({
method: "POST",
url: "/api/users/import/dry-run",
...multipartRequest,
});
expect(response.statusCode).toBe(403);
expect(departmentFindMany).not.toHaveBeenCalled();
expect(employeeFindMany).not.toHaveBeenCalled();
expect(auditCreate).not.toHaveBeenCalled();
await app.close();
});
it("supports xlsx dry-run and allows manager references to later rows in the same file", async () => {
const { app } = await buildApp({
departments: [{ id: "dept-1", name: "行政部", archived_at: null }],
@@ -321,6 +351,45 @@ describe("users import routes", () => {
await app.close();
});
it.each(["admin", "system_admin"])(
"prevents a system administrator from importing the protected %s role",
async (protectedRole) => {
const {
app,
auditCreate,
departmentFindMany,
employeeCreate,
employeeFindMany,
roleCreateMany,
} = await buildApp({
departments: [{ id: "dept-1", name: "行政部", archived_at: null }],
});
const multipartRequest = buildMultipartRequest({
filename: "users.csv",
contentType: "text/csv",
content: [
"employeeNo,name,phone,email,departmentName,title,directManagerEmployeeNo,roles,hireDate",
`E004,Dana,13800000004,[email protected],行政部,,,${protectedRole},2026-03-29`,
].join("\n"),
});
const response = await app.inject({
method: "POST",
url: "/api/users/import/apply",
...multipartRequest,
});
expect(response.statusCode).toBe(403);
expect(departmentFindMany).toHaveBeenCalledTimes(1);
expect(employeeFindMany).toHaveBeenCalledTimes(1);
expect(employeeCreate).not.toHaveBeenCalled();
expect(roleCreateMany).not.toHaveBeenCalled();
expect(auditCreate).not.toHaveBeenCalled();
await app.close();
},
);
it("refuses apply when preview contains invalid rows and does not create employees", async () => {
const { app, employeeCreate } = await buildApp();
const multipartRequest = buildMultipartRequest({