158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../db.server", () => ({
|
|
db: {
|
|
role: {
|
|
findUnique: vi.fn(),
|
|
findMany: vi.fn(),
|
|
},
|
|
permission: {
|
|
findMany: vi.fn(),
|
|
},
|
|
rolePermission: {
|
|
create: vi.fn(),
|
|
createMany: vi.fn(),
|
|
delete: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
findMany: vi.fn(),
|
|
},
|
|
$transaction: vi.fn(),
|
|
},
|
|
}));
|
|
vi.mock("~/utils/cache.server", () => ({ clearCacheKey: vi.fn() }));
|
|
vi.mock("../tenants.db.server", () => ({ adminGetAllTenants: vi.fn() }));
|
|
vi.mock("../users.db.server", () => ({ adminGetAllUsersNames: vi.fn() }));
|
|
|
|
import { clearCacheKey } from "~/utils/cache.server";
|
|
import { adminGetAllTenants } from "../tenants.db.server";
|
|
import { adminGetAllUsersNames } from "../users.db.server";
|
|
import { db } from "../../db.server";
|
|
import {
|
|
createRolePermission,
|
|
deleteRolePermission,
|
|
deleteRolePermissionById,
|
|
getAllRolePermissions,
|
|
setPermissionRoles,
|
|
setRolePermissions,
|
|
} from "./rolePermissions.db.server";
|
|
|
|
describe("role permission persistence", () => {
|
|
const dbMock = vi.mocked(db);
|
|
const findRoleByIdMock = vi.mocked(dbMock.role.findUnique);
|
|
const findPermissionsMock = vi.mocked(dbMock.permission.findMany);
|
|
const findRolesMock = vi.mocked(dbMock.role.findMany);
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
dbMock.$transaction.mockImplementation(async (callback: (tx: typeof dbMock) => Promise<unknown>) => callback(dbMock));
|
|
vi.mocked(adminGetAllTenants).mockResolvedValue([]);
|
|
vi.mocked(adminGetAllUsersNames).mockResolvedValue([]);
|
|
});
|
|
|
|
it("sets role permissions inside one awaited transaction before clearing caches", async () => {
|
|
findRoleByIdMock.mockResolvedValue({ id: "role_1", name: "Editor", type: "app" } as never);
|
|
findPermissionsMock.mockResolvedValue([{ id: "permission_a" }, { id: "permission_b" }] as never);
|
|
|
|
await setRolePermissions("role_1", ["settings.update", "settings.update", "members.read"]);
|
|
|
|
expect(dbMock.permission.findMany).toHaveBeenCalledWith({
|
|
where: { name: { in: ["settings.update", "members.read"] } },
|
|
select: { id: true },
|
|
});
|
|
expect(dbMock.$transaction).toHaveBeenCalledTimes(1);
|
|
expect(dbMock.rolePermission.deleteMany).toHaveBeenCalledWith({ where: { roleId: "role_1" } });
|
|
expect(dbMock.rolePermission.createMany).toHaveBeenCalledWith({
|
|
data: [
|
|
{ roleId: "role_1", permissionId: "permission_a" },
|
|
{ roleId: "role_1", permissionId: "permission_b" },
|
|
],
|
|
});
|
|
expect(clearCacheKey).toHaveBeenCalledWith("roles:app");
|
|
});
|
|
|
|
it("reads and creates role-permission links without duplicating existing links", async () => {
|
|
dbMock.rolePermission.findMany.mockResolvedValueOnce([{ id: "rp_1" }] as never);
|
|
dbMock.rolePermission.findFirst
|
|
.mockResolvedValueOnce({ id: "rp_existing" } as never)
|
|
.mockResolvedValueOnce(null);
|
|
dbMock.rolePermission.create.mockResolvedValueOnce({ id: "rp_created" } as never);
|
|
|
|
await expect(getAllRolePermissions()).resolves.toEqual([{ id: "rp_1" }]);
|
|
await expect(createRolePermission({ roleId: "role_1", permissionId: "permission_1" })).resolves.toEqual({ id: "rp_existing" });
|
|
await expect(createRolePermission({ roleId: "role_1", permissionId: "permission_2" })).resolves.toEqual({ id: "rp_created" });
|
|
|
|
expect(dbMock.rolePermission.create).toHaveBeenCalledTimes(1);
|
|
expect(dbMock.rolePermission.create).toHaveBeenCalledWith({ data: { roleId: "role_1", permissionId: "permission_2" } });
|
|
});
|
|
|
|
it("returns early when setting permissions for a missing role", async () => {
|
|
findRoleByIdMock.mockResolvedValueOnce(null);
|
|
|
|
await setRolePermissions("role_missing", ["settings.update"]);
|
|
|
|
expect(dbMock.permission.findMany).not.toHaveBeenCalled();
|
|
expect(dbMock.$transaction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("clears app role user caches across tenants and user-role lookup caches", async () => {
|
|
findRoleByIdMock.mockResolvedValue({ id: "role_1", name: "Member", type: "app" } as never);
|
|
findPermissionsMock.mockResolvedValue([]);
|
|
vi.mocked(adminGetAllTenants).mockResolvedValue([
|
|
{ id: "tenant_1", users: [{ userId: "user_1" }, { userId: "user_2" }] },
|
|
{ id: "tenant_2", users: [{ userId: "user_3" }] },
|
|
] as never);
|
|
vi.mocked(adminGetAllUsersNames).mockResolvedValue([{ id: "user_1" }, { id: "user_4" }] as never);
|
|
|
|
await setRolePermissions("role_1", []);
|
|
|
|
expect(dbMock.rolePermission.createMany).not.toHaveBeenCalled();
|
|
expect(clearCacheKey).toHaveBeenCalledWith("roles:app");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("userRoles:user_1:tenant_1");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("userRoles:user_2:tenant_1");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("userRoles:user_3:tenant_2");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("userRole:user_1:Member");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("userRole:user_4:Member");
|
|
});
|
|
|
|
it("sets permission roles inside one awaited transaction", async () => {
|
|
findRolesMock.mockResolvedValue([{ id: "role_a" }, { id: "role_b" }] as never);
|
|
|
|
await setPermissionRoles("permission_1", ["Admin", "Admin", "Member"]);
|
|
|
|
expect(dbMock.role.findMany).toHaveBeenCalledWith({
|
|
where: { name: { in: ["Admin", "Member"] } },
|
|
select: { id: true },
|
|
});
|
|
expect(dbMock.$transaction).toHaveBeenCalledTimes(1);
|
|
expect(dbMock.rolePermission.deleteMany).toHaveBeenCalledWith({ where: { permissionId: "permission_1" } });
|
|
expect(dbMock.rolePermission.createMany).toHaveBeenCalledWith({
|
|
data: [
|
|
{ roleId: "role_a", permissionId: "permission_1" },
|
|
{ roleId: "role_b", permissionId: "permission_1" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("clears permission role links without creating rows when role list is empty", async () => {
|
|
await setPermissionRoles("permission_1", []);
|
|
|
|
expect(dbMock.role.findMany).not.toHaveBeenCalled();
|
|
expect(dbMock.$transaction).toHaveBeenCalledTimes(1);
|
|
expect(dbMock.rolePermission.deleteMany).toHaveBeenCalledWith({ where: { permissionId: "permission_1" } });
|
|
expect(dbMock.rolePermission.createMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("deletes role-permission links by composite keys or id", async () => {
|
|
dbMock.rolePermission.deleteMany.mockResolvedValueOnce({ count: 1 } as never);
|
|
dbMock.rolePermission.delete.mockResolvedValueOnce({ id: "rp_1" } as never);
|
|
|
|
await expect(deleteRolePermission("role_1", "permission_1")).resolves.toEqual({ count: 1 });
|
|
await expect(deleteRolePermissionById("rp_1")).resolves.toEqual({ id: "rp_1" });
|
|
|
|
expect(dbMock.rolePermission.deleteMany).toHaveBeenCalledWith({ where: { roleId: "role_1", permissionId: "permission_1" } });
|
|
expect(dbMock.rolePermission.delete).toHaveBeenCalledWith({ where: { id: "rp_1" } });
|
|
});
|
|
});
|