Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/services/userService.admin-rbac.test.ts
T

113 lines
3.9 KiB
TypeScript

// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from "vitest";
import { runWithRlsContext } from "@hivegrid/db/rls-context.server";
const transaction = vi.hoisted(() => ({
role: { findMany: vi.fn() },
userRole: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn() },
}));
const dbMock = vi.hoisted(() => ({
$transaction: vi.fn(async (callback: (client: typeof transaction) => Promise<unknown>) => callback(transaction)),
}));
const invalidatePermissionScopesMock = vi.hoisted(() => vi.fn());
const invalidateSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("../db.server", () => ({ db: dbMock }));
vi.mock("../db/users.db.server", () => ({ deleteUser: vi.fn() }));
vi.mock("../db/permissions/permission-cache.server", () => ({
invalidateUserPermissionCacheScopes: invalidatePermissionScopesMock,
}));
vi.mock("../db/user-snapshot-cache.server", () => ({ invalidateUserSnapshot: invalidateSnapshotMock }));
vi.mock("./tenantService", () => ({ deleteAndCancelTenant: vi.fn() }));
vi.mock("~/modules/events/services/.server/EventsService", () => ({ default: {} }));
import { setUserRoles } from "./userService";
const user = {
id: "user_1",
email: "member@example.test",
firstName: "Ada",
lastName: "Member",
avatar: null,
phone: null,
defaultTenantId: null,
createdAt: new Date("2026-07-14T08:00:00.000Z"),
locale: "de",
};
const adminRole = {
id: "role_admin",
tenantId: null,
type: "admin",
name: "SuperAdmin",
};
const appRole = {
id: "role_app",
tenantId: null,
type: "app",
name: "Office",
};
describe("tenant UserRole mutation boundary", () => {
beforeEach(() => {
vi.clearAllMocks();
transaction.userRole.findMany.mockResolvedValue([]);
transaction.userRole.deleteMany.mockResolvedValue({ count: 0 });
transaction.userRole.createMany.mockResolvedValue({ count: 1 });
});
it("rejects the retired UserRole-based admin path before writing", async () => {
transaction.role.findMany.mockResolvedValue([{ id: adminRole.id, tenantId: null, type: "admin" }]);
await expect(
runWithRlsContext({ userId: "operator_1" }, () =>
setUserRoles({
user,
roles: [{ role: adminRole as never, tenantId: null }],
type: "admin",
}),
),
).rejects.toThrow("Legacy UserRole-based admin assignment is disabled");
expect(transaction.userRole.findMany).not.toHaveBeenCalled();
expect(transaction.userRole.deleteMany).not.toHaveBeenCalled();
expect(transaction.userRole.createMany).not.toHaveBeenCalled();
expect(invalidatePermissionScopesMock).not.toHaveBeenCalled();
expect(invalidateSnapshotMock).not.toHaveBeenCalled();
});
it("rejects tenant roles outside the exact active tenant context", async () => {
transaction.role.findMany.mockResolvedValue([{ id: appRole.id, tenantId: null, type: "app" }]);
await expect(
runWithRlsContext({ userId: "operator_1" }, () =>
setUserRoles({
user,
roles: [{ role: appRole as never, tenantId: "tenant_1" }],
type: "app",
}),
),
).rejects.toThrow("matching active tenant context");
expect(transaction.userRole.deleteMany).not.toHaveBeenCalled();
});
it("replaces app roles and invalidates only the bound tenant scope", async () => {
transaction.role.findMany.mockResolvedValue([{ id: appRole.id, tenantId: null, type: "app" }]);
await runWithRlsContext({ userId: "operator_1", tenantId: "tenant_1" }, () =>
setUserRoles({
user,
roles: [{ role: appRole as never, tenantId: "tenant_1" }],
type: "app",
}),
);
expect(transaction.userRole.createMany).toHaveBeenCalledWith({
data: [{ userId: user.id, roleId: appRole.id, tenantId: "tenant_1" }],
});
expect(invalidatePermissionScopesMock).toHaveBeenCalledWith([{ userId: user.id, tenantId: "tenant_1" }]);
expect(invalidateSnapshotMock).toHaveBeenCalledWith(user.id);
});
});