208 lines
9.5 KiB
TypeScript
208 lines
9.5 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../db.server", () => ({
|
|
db: {
|
|
permission: {
|
|
aggregate: vi.fn(),
|
|
delete: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
create: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
vi.mock("./roles.db.server", () => ({ getAllRolesNames: vi.fn(), getRoleByName: vi.fn() }));
|
|
vi.mock("./rolePermissions.db.server", () => ({ createRolePermission: vi.fn(), getAllRolePermissions: vi.fn() }));
|
|
vi.mock("~/utils/cache.server", () => ({ cachified: vi.fn(), clearCacheKey: vi.fn() }));
|
|
vi.mock("~/utils/helpers/PermissionsHelper", () => ({ getEntityPermissions: vi.fn() }));
|
|
vi.mock("~/utils/helpers/RowFiltersHelper", () => ({ default: { getFiltersCondition: vi.fn() } }));
|
|
|
|
import { cachified, clearCacheKey } from "~/utils/cache.server";
|
|
import { getEntityPermissions } from "~/utils/helpers/PermissionsHelper";
|
|
import RowFiltersHelper from "~/utils/helpers/RowFiltersHelper";
|
|
import { db } from "../../db.server";
|
|
import { createRolePermission, getAllRolePermissions } from "./rolePermissions.db.server";
|
|
import { getAllRolesNames, getRoleByName } from "./roles.db.server";
|
|
import {
|
|
createEntityPermissions,
|
|
createPermissions,
|
|
deleteEntityPermissions,
|
|
deletePermission,
|
|
getAllPermissions,
|
|
getAllPermissionsIdsAndNames,
|
|
getNextPermissionsOrder,
|
|
getPermission,
|
|
getPermissionByName,
|
|
getPermissionByNames,
|
|
getPermissionName,
|
|
updatePermission,
|
|
} from "./permissions.db.server";
|
|
|
|
describe("permission persistence", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(cachified).mockImplementation((async (options: { getFreshValue: () => unknown }) =>
|
|
options.getFreshValue()) as never);
|
|
});
|
|
|
|
it("lists permissions with filters, role filters, and type constraints", async () => {
|
|
vi.mocked(RowFiltersHelper.getFiltersCondition).mockReturnValue({ name: { contains: "admin" } } as never);
|
|
vi.mocked(db.permission.findMany).mockResolvedValueOnce([] as never);
|
|
|
|
await getAllPermissions("admin", {
|
|
properties: [{ name: "roleId", value: "role_1" }],
|
|
} as never);
|
|
|
|
expect(db.permission.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
AND: ["admin", { OR: [{ name: { contains: "admin" } }, { inRoles: { some: { roleId: "role_1" } } }] }],
|
|
},
|
|
include: { inRoles: { include: { role: true } } },
|
|
orderBy: [{ type: "asc" }, { name: "asc" }],
|
|
});
|
|
});
|
|
|
|
it("queries permission lookup variants and cached permission names", async () => {
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([] as never);
|
|
vi.mocked(db.permission.findUnique).mockResolvedValue({ id: "permission_1" } as never);
|
|
|
|
await getAllPermissionsIdsAndNames();
|
|
await getPermission("permission_1");
|
|
await getPermissionByName("admin.users.view");
|
|
await getPermissionName("admin.users.view");
|
|
await getPermissionByNames(["admin.users.view", "admin.users.edit"]);
|
|
|
|
expect(db.permission.findMany).toHaveBeenNthCalledWith(1, {
|
|
select: { id: true, name: true },
|
|
orderBy: { name: "asc" },
|
|
});
|
|
expect(db.permission.findUnique).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({ where: { id: "permission_1" } })
|
|
);
|
|
expect(db.permission.findUnique).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({ where: { name: "admin.users.view" } })
|
|
);
|
|
expect(cachified).toHaveBeenCalledWith(expect.objectContaining({ key: "permission:admin.users.view" }));
|
|
expect(db.permission.findMany).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({ where: { name: { in: ["admin.users.view", "admin.users.edit"] } } })
|
|
);
|
|
});
|
|
|
|
it("calculates the next permission order with and without a type filter", async () => {
|
|
vi.mocked(db.permission.aggregate)
|
|
.mockResolvedValueOnce({ _max: { order: 4 } } as never)
|
|
.mockResolvedValueOnce({ _max: { order: null } } as never);
|
|
|
|
await expect(getNextPermissionsOrder("admin")).resolves.toBe(5);
|
|
await expect(getNextPermissionsOrder()).resolves.toBe(1);
|
|
|
|
expect(db.permission.aggregate).toHaveBeenNthCalledWith(1, { where: { type: "admin" }, _max: { order: true } });
|
|
expect(db.permission.aggregate).toHaveBeenNthCalledWith(2, { where: {}, _max: { order: true } });
|
|
});
|
|
|
|
it("repairs role links for existing permissions instead of returning early", async () => {
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([{ id: "permission_1", name: "admin.users.view" }] as never);
|
|
vi.mocked(getAllRolePermissions).mockResolvedValue([]);
|
|
vi.mocked(getRoleByName).mockResolvedValue({ id: "role_1", name: "SuperAdmin" } as never);
|
|
vi.mocked(createRolePermission).mockResolvedValue({ id: "role_permission_1" } as never);
|
|
|
|
await createPermissions([{ name: "admin.users.view", description: "View users", type: "admin", inRoles: ["SuperAdmin"] }]);
|
|
|
|
expect(db.permission.create).not.toHaveBeenCalled();
|
|
expect(createRolePermission).toHaveBeenCalledWith({ permissionId: "permission_1", roleId: "role_1" });
|
|
});
|
|
|
|
it("clears the permission lookup cache after creating a previously missing permission", async () => {
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([]);
|
|
vi.mocked(db.permission.create).mockResolvedValue({ id: "permission_1", name: "admin.users.view" } as never);
|
|
vi.mocked(getAllRolePermissions).mockResolvedValue([]);
|
|
vi.mocked(getRoleByName).mockResolvedValue({ id: "role_1", name: "SuperAdmin" } as never);
|
|
vi.mocked(createRolePermission).mockResolvedValue({ id: "role_permission_1" } as never);
|
|
|
|
await createPermissions([{ name: "admin.users.view", description: "View users", type: "admin", inRoles: ["SuperAdmin"] }]);
|
|
|
|
expect(clearCacheKey).toHaveBeenCalledWith("permission:admin.users.view");
|
|
expect(createRolePermission).toHaveBeenCalledWith({ permissionId: "permission_1", roleId: "role_1" });
|
|
});
|
|
|
|
it("skips duplicate permission inputs after creating the first instance", async () => {
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([]);
|
|
vi.mocked(db.permission.create).mockResolvedValue({ id: "permission_1", name: "admin.users.view" } as never);
|
|
vi.mocked(getAllRolePermissions).mockResolvedValue([]);
|
|
vi.mocked(getRoleByName).mockResolvedValue({ id: "role_1", name: "SuperAdmin" } as never);
|
|
|
|
await createPermissions([
|
|
{ name: "admin.users.view", description: "View users", type: "admin", inRoles: ["SuperAdmin"] },
|
|
{ name: "admin.users.view", description: "View users", type: "admin", inRoles: ["SuperAdmin"] },
|
|
]);
|
|
|
|
expect(db.permission.create).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("throws when a requested role link cannot be resolved", async () => {
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([]);
|
|
vi.mocked(db.permission.create).mockResolvedValue({ id: "permission_1", name: "admin.users.view" } as never);
|
|
vi.mocked(getAllRolePermissions).mockResolvedValue([]);
|
|
vi.mocked(getRoleByName).mockResolvedValue(null);
|
|
|
|
await expect(
|
|
createPermissions([{ name: "admin.users.view", description: "View users", type: "admin", inRoles: ["Missing"] }])
|
|
).rejects.toThrow("Role required: Missing");
|
|
});
|
|
|
|
it("updates and deletes permissions while clearing the permission cache", async () => {
|
|
vi.mocked(db.permission.update).mockResolvedValueOnce({ id: "permission_1", name: "admin.users.view" } as never);
|
|
vi.mocked(db.permission.delete).mockResolvedValueOnce({ id: "permission_1", name: "admin.users.delete" } as never);
|
|
|
|
await updatePermission("permission_1", { name: "admin.users.view" });
|
|
await deletePermission("permission_1");
|
|
|
|
expect(clearCacheKey).toHaveBeenCalledWith("permission:admin.users.view");
|
|
expect(clearCacheKey).toHaveBeenCalledWith("permission:admin.users.delete");
|
|
});
|
|
|
|
it("deletes entity permissions only when generated permission names exist", async () => {
|
|
vi.mocked(getEntityPermissions)
|
|
.mockResolvedValueOnce([{ name: "entity.view" }, { name: "entity.edit" }] as never)
|
|
.mockResolvedValueOnce([] as never);
|
|
vi.mocked(db.permission.deleteMany).mockResolvedValueOnce({ count: 2 } as never);
|
|
|
|
await expect(deleteEntityPermissions({ id: "entity_1" } as never)).resolves.toEqual({ count: 2 });
|
|
await expect(deleteEntityPermissions({ id: "entity_1" } as never)).resolves.toBeUndefined();
|
|
|
|
expect(db.permission.deleteMany).toHaveBeenCalledTimes(1);
|
|
expect(db.permission.deleteMany).toHaveBeenCalledWith({
|
|
where: { name: { in: ["entity.view", "entity.edit"] } },
|
|
});
|
|
});
|
|
|
|
it("creates entity permissions for every generated permission and all app roles", async () => {
|
|
vi.mocked(getAllRolesNames).mockResolvedValue([{ id: "role_1", name: "Member" }] as never);
|
|
vi.mocked(getEntityPermissions).mockResolvedValue([{ name: "entity.view", description: "View entity" }] as never);
|
|
vi.mocked(getAllRolePermissions).mockResolvedValue([]);
|
|
vi.mocked(db.permission.findMany).mockResolvedValue([]);
|
|
vi.mocked(db.permission.create).mockResolvedValue({ id: "permission_1", name: "entity.view" } as never);
|
|
vi.mocked(getRoleByName).mockResolvedValue({ id: "role_1", name: "Member" } as never);
|
|
|
|
await createEntityPermissions({ id: "entity_1" } as never);
|
|
|
|
expect(db.permission.create).toHaveBeenCalledWith({
|
|
data: {
|
|
order: 2,
|
|
name: "entity.view",
|
|
description: "View entity",
|
|
type: "app",
|
|
isDefault: true,
|
|
entityId: "entity_1",
|
|
},
|
|
});
|
|
expect(createRolePermission).toHaveBeenCalledWith({ permissionId: "permission_1", roleId: "role_1" });
|
|
});
|
|
});
|