180 lines
6.8 KiB
TypeScript
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();
|
|
});
|
|
});
|