Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/workspaceRoles.server.test.ts
T

180 lines
6.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("~/utils/services/.server/featureFlagsRequest.server", () => ({
isFeatureFlagEnabled: vi.fn(),
}));
vi.mock("~/utils/helpers/.server/PermissionsService", () => ({
verifyUserHasPermission: vi.fn(),
}));
vi.mock("~/locale/i18next.server", () => ({
getTranslations: vi.fn(async () => ({
t: (key: string, values?: Record<string, unknown>) => (values ? `${key}:${JSON.stringify(values)}` : key),
})),
}));
vi.mock("~/utils/db/permissions/permissions.db.server", () => ({
getAllPermissions: vi.fn(),
}));
vi.mock("~/utils/db/permissions/rolePermissions.db.server", () => ({
setRolePermissions: vi.fn(),
}));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({
createRole: vi.fn(),
deleteRole: vi.fn(),
getNextRolesOrder: vi.fn(),
getRole: vi.fn(),
getTenantAppRoleByName: vi.fn(),
updateRole: vi.fn(),
}));
vi.mock("~/utils/db/logs.db.server", () => ({
createAdminLog: vi.fn(),
}));
import { createAdminLog } from "~/utils/db/logs.db.server";
import { getAllPermissions } from "~/utils/db/permissions/permissions.db.server";
import { setRolePermissions } from "~/utils/db/permissions/rolePermissions.db.server";
import { createRole, deleteRole, getNextRolesOrder, getRole, getTenantAppRoleByName, updateRole } from "~/utils/db/permissions/roles.db.server";
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
import { createWorkspaceRole, deleteWorkspaceRole, requireWorkspaceRolesBuilderAccess, updateWorkspaceRole } from "./workspaceRoles.server";
const request = new Request("https://example.com/acme/settings/roles?token=secret&password=hunter2");
const params = { tenant: "acme" };
const tenantId = "tenant_1";
async function expectResponseRejection(promise: Promise<unknown>, status: number, text: string) {
await expect(promise).rejects.toMatchObject({ status });
await promise.catch(async (error) => {
expect(error).toBeInstanceOf(Response);
await expect((error as Response).text()).resolves.toContain(text);
});
}
function expectNoRoleMutations() {
expect(createRole).not.toHaveBeenCalled();
expect(updateRole).not.toHaveBeenCalled();
expect(deleteRole).not.toHaveBeenCalled();
expect(setRolePermissions).not.toHaveBeenCalled();
expect(createAdminLog).not.toHaveBeenCalled();
}
describe("workspace role builder policy", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(verifyUserHasPermission).mockResolvedValue(true);
vi.mocked(getNextRolesOrder).mockResolvedValue(7);
vi.mocked(getTenantAppRoleByName).mockResolvedValue(null);
vi.mocked(getAllPermissions).mockResolvedValue([{ name: "app.settings.members.view", type: "app" }] as never);
});
it("requires the role-setting permission (no feature-flag gate)", async () => {
await expect(requireWorkspaceRolesBuilderAccess({ request, params, tenantId })).resolves.toEqual({ tenantId });
expect(verifyUserHasPermission).toHaveBeenCalledWith(request, "app.settings.roles.set", tenantId);
});
it("creates tenant-owned workspace roles with app permissions", async () => {
vi.mocked(createRole).mockResolvedValue({ id: "role_1", name: "Workspace editor", type: "app" } as never);
await expect(
createWorkspaceRole({
request,
tenantId,
data: { name: " Workspace editor ", description: "Can edit workspace data", type: "app", permissionNames: ["app.settings.members.view"] },
})
).resolves.toMatchObject({ id: "role_1" });
expect(createRole).toHaveBeenCalledWith({
order: 7,
name: "Workspace editor",
description: "Can edit workspace data",
assignToNewUsers: false,
type: "app",
isDefault: false,
tenantId,
});
expect(setRolePermissions).toHaveBeenCalledWith("role_1", ["app.settings.members.view"]);
});
it("still rejects admin role type tampering before storage checks", async () => {
await expectResponseRejection(
createWorkspaceRole({
request,
tenantId,
data: { name: "Admin", description: "", type: "admin", permissionNames: ["app.settings.members.view"] },
}),
400,
"settings.rolesAndPermissions.errors.appRolesOnly",
);
expectNoRoleMutations();
});
it("updates and deletes only tenant-owned non-default app roles", async () => {
vi.mocked(getRole).mockResolvedValue({ id: "role_1", name: "Workspace editor", type: "app", isDefault: false, tenantId, permissions: [] } as never);
vi.mocked(updateRole).mockResolvedValue({ id: "role_1", name: "Workspace editor", type: "app" } as never);
vi.mocked(deleteRole).mockResolvedValue({ id: "role_1", name: "Workspace editor", type: "app" } as never);
await updateWorkspaceRole({
request,
tenantId,
roleId: "role_1",
data: { name: "Workspace editor", description: "Updated", type: "app", permissionNames: ["app.settings.members.view"] },
});
await deleteWorkspaceRole({ request, tenantId, roleId: "role_1" });
expect(updateRole).toHaveBeenCalledWith("role_1", {
name: "Workspace editor",
description: "Updated",
assignToNewUsers: false,
type: "app",
});
expect(deleteRole).toHaveBeenCalledWith("role_1");
});
it("blocks role updates outside the current tenant", async () => {
vi.mocked(getRole).mockResolvedValue({ id: "role_1", name: "Other tenant", type: "app", isDefault: false, tenantId: "tenant_2", permissions: [] } as never);
await expectResponseRejection(
updateWorkspaceRole({
request,
tenantId,
roleId: "role_1",
data: { name: "Workspace editor", description: "Updated", type: "app", permissionNames: ["app.settings.members.view"] },
}),
404,
"settings.rolesAndPermissions.errors.roleNotFound"
);
expect(updateRole).not.toHaveBeenCalled();
});
it("rejects duplicate role names with localized interpolation before storage mutation", async () => {
vi.mocked(getTenantAppRoleByName).mockResolvedValueOnce({ id: "role_existing", name: "Workspace editor", type: "app" } as never);
await expectResponseRejection(
createWorkspaceRole({
request,
tenantId,
data: { name: "Workspace editor", description: "", type: "app", permissionNames: ["app.settings.members.view"] },
}),
400,
'settings.rolesAndPermissions.errors.existingRole:{"name":"Workspace editor"}',
);
expectNoRoleMutations();
});
it("rejects non-app permission names with localized interpolation before storage mutation", async () => {
await expectResponseRejection(
createWorkspaceRole({
request,
tenantId,
data: { name: "Workspace editor", description: "", type: "app", permissionNames: ["admin.roles.delete"] },
}),
400,
'settings.rolesAndPermissions.errors.invalidAppPermissions:{"permissions":"admin.roles.delete"}',
);
expectNoRoleMutations();
});
});