完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -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({
|
||||
|
||||
在新工单中引用
屏蔽一个用户