Files
massive-cloud/server/users.security.test.ts

62 lines
2.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { User } from "../drizzle/schema";
import type { TrpcContext } from "./_core/context";
vi.mock("./db", () => ({
getUserById: vi.fn(),
countActiveSuperAdmins: vi.fn(),
updateUser: vi.fn(),
deleteUser: vi.fn(),
}));
import * as db from "./db";
import { appRouter } from "./routers";
function makeUser(id: number, role: User["role"] = "super_admin"): User {
const now = new Date();
return { id, username: `user-${id}`, passwordHash: "unused", name: `User ${id}`, email: null, role, isActive: 1, mustChangePassword: 0, sessionVersion: 0, createdAt: now, updatedAt: now, lastLoginAt: null };
}
function caller() {
const ctx: TrpcContext = { user: makeUser(1), req: {} as TrpcContext["req"], res: {} as TrpcContext["res"] };
return appRouter.createCaller(ctx);
}
describe("用户管理安全保护", () => {
beforeEach(() => vi.clearAllMocks());
it("禁止删除自己", async () => {
await expect(caller().users.delete({ id: 1 })).rejects.toMatchObject({ code: "FORBIDDEN" });
expect(db.deleteUser).not.toHaveBeenCalled();
});
it("禁止修改自己的角色", async () => {
vi.mocked(db.getUserById).mockResolvedValue(makeUser(1));
await expect(caller().users.update({ id: 1, role: "admin" })).rejects.toMatchObject({ code: "FORBIDDEN" });
expect(db.updateUser).not.toHaveBeenCalled();
});
it("禁止禁用最后一个有效超级管理员", async () => {
vi.mocked(db.getUserById).mockResolvedValue(makeUser(2));
vi.mocked(db.countActiveSuperAdmins).mockResolvedValue(1);
await expect(caller().users.setActive({ id: 2, isActive: false })).rejects.toMatchObject({ code: "FORBIDDEN" });
expect(db.updateUser).not.toHaveBeenCalled();
});
it("有多个超级管理员时允许禁用其中一个并使旧会话失效", async () => {
const target = makeUser(2);
vi.mocked(db.getUserById).mockResolvedValue(target);
vi.mocked(db.countActiveSuperAdmins).mockResolvedValue(2);
vi.mocked(db.updateUser).mockResolvedValue({ ...target, isActive: 0, sessionVersion: 1 });
await expect(caller().users.setActive({ id: 2, isActive: false })).resolves.toMatchObject({ isActive: 0, sessionVersion: 1 });
expect(db.updateUser).toHaveBeenCalledWith(2, { isActive: 0, sessionVersion: 1 });
});
it("遗留账号未修改用户名并重置密码前不能启用", async () => {
const legacy = { ...makeUser(2, "user"), username: "legacy_user_2", passwordHash: null, isActive: 0 };
vi.mocked(db.getUserById).mockResolvedValue(legacy);
await expect(caller().users.setActive({ id: 2, isActive: true })).rejects.toMatchObject({ code: "FORBIDDEN" });
expect(db.updateUser).not.toHaveBeenCalled();
});
});