113 lines
3.9 KiB
TypeScript
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);
|
|
});
|
|
});
|