Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/rolesAndPermissionsService.seed-diagnostics.test.ts
T

253 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("~/utils/db/entities/entities.db.server", () => ({ getAllEntities: vi.fn() }));
vi.mock("~/utils/db/permissions/roles.db.server", () => ({ createRole: vi.fn(), getAllRoles: vi.fn() }));
vi.mock("~/utils/db/permissions/userRoles.db.server", () => ({ createUserRoleAssignments: vi.fn() }));
vi.mock("~/utils/db/tenants.db.server", () => ({ adminGetAllTenants: vi.fn() }));
vi.mock("~/utils/db/users.db.server", () => ({ adminGetAllUsers: vi.fn() }));
vi.mock("~/utils/db.server", () => ({
db: {
role: {
findMany: vi.fn(),
},
permission: {
createMany: vi.fn(),
findMany: vi.fn(),
},
rolePermission: {
createMany: vi.fn(),
},
},
}));
vi.mock("~/utils/cache.server", () => ({ clearCacheKey: vi.fn() }));
vi.mock("~/utils/helpers/PermissionsHelper", () => ({ getEntityPermissions: vi.fn() }));
vi.mock("@toolctrl/db/permissions-seed.server", async () => {
const actual = await vi.importActual<typeof import("@toolctrl/db/permissions-seed.server")>("@toolctrl/db/permissions-seed.server");
return {
...actual,
syncDefaultRolesAndPermissions: vi.fn(async () => ({
roleCount: actual.defaultAdminRoleSeeds.length + actual.defaultAppRoleSeeds.length,
permissionCount: actual.defaultPermissionSeeds.length,
createdPermissions: 0,
createdRolePermissions: 0,
})),
};
});
import { getAllEntities } from "~/utils/db/entities/entities.db.server";
import { getAllRoles } from "~/utils/db/permissions/roles.db.server";
import { createUserRoleAssignments } from "~/utils/db/permissions/userRoles.db.server";
import { adminGetAllTenants } from "~/utils/db/tenants.db.server";
import { adminGetAllUsers } from "~/utils/db/users.db.server";
import { db } from "~/utils/db.server";
import { clearCacheKey } from "~/utils/cache.server";
import { getEntityPermissions } from "~/utils/helpers/PermissionsHelper";
import { syncDefaultRolesAndPermissions } from "@toolctrl/db/permissions-seed.server";
import { DefaultAdminRoles } from "~/application/dtos/shared/DefaultAdminRoles";
import { DefaultAppRoles } from "~/application/dtos/shared/DefaultAppRoles";
import { defaultAdminRoles, defaultAppRoles, defaultPermissions, seedRolesAndPermissions, type RolesAndPermissionsSeedDiagnosticEvent } from "./rolesAndPermissionsService";
const getAllEntitiesMock = vi.mocked(getAllEntities);
const getAllRolesMock = vi.mocked(getAllRoles);
const createUserRoleAssignmentsMock = vi.mocked(createUserRoleAssignments);
const adminGetAllTenantsMock = vi.mocked(adminGetAllTenants);
const adminGetAllUsersMock = vi.mocked(adminGetAllUsers);
const dbMock = vi.mocked(db);
const clearCacheKeyMock = vi.mocked(clearCacheKey);
const getEntityPermissionsMock = vi.mocked(getEntityPermissions);
const syncDefaultRolesAndPermissionsMock = vi.mocked(syncDefaultRolesAndPermissions);
type RoleRow = Awaited<ReturnType<typeof getAllRoles>>[number];
const roleBase = {
type: "admin",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
description: "Test role",
order: 0,
isDefault: true,
assignToNewUsers: false,
permissions: [],
};
const adminRoleRows = defaultAdminRoles.map((role, index) => ({ ...roleBase, id: `admin-role-${index}`, name: role.name, type: role.type }) as RoleRow);
const appRoleRows = defaultAppRoles.map((role, index) => ({ ...roleBase, id: `app-role-${index}`, name: role.name, type: role.type }) as RoleRow);
const allRoleRows = [...adminRoleRows, ...appRoleRows];
function serializedDiagnostics(events: RolesAndPermissionsSeedDiagnosticEvent[]) {
return JSON.stringify(events);
}
describe("seedRolesAndPermissions diagnostics", () => {
beforeEach(() => {
vi.clearAllMocks();
getAllRolesMock.mockImplementation(async (type?: string) => {
if (type === "admin") return adminRoleRows;
if (type === "app") return appRoleRows;
return allRoleRows;
});
dbMock.permission.createMany.mockResolvedValue({ count: 3 } as never);
dbMock.permission.findMany.mockResolvedValue([
{ id: "permission-widget-view", name: "app.widget.view" },
{ id: "permission-report-view", name: "app.report.view" },
{ id: "permission-report-read", name: "app.report.read" },
] as never);
dbMock.role.findMany.mockResolvedValue(
[
{ id: "admin-role-super-admin", name: DefaultAdminRoles.SuperAdmin },
{ id: "app-role-super-user", name: DefaultAppRoles.SuperUser },
{ id: "app-role-admin", name: DefaultAppRoles.Admin },
{ id: "app-role-user", name: DefaultAppRoles.User },
] as never
);
dbMock.rolePermission.createMany.mockResolvedValue({ count: 12 } as never);
getAllEntitiesMock.mockResolvedValue([
{ id: "entity-1", type: "widget" } as Awaited<ReturnType<typeof getAllEntities>>[number],
{ id: "entity-2", type: "report" } as Awaited<ReturnType<typeof getAllEntities>>[number],
]);
getEntityPermissionsMock
.mockResolvedValueOnce([{ name: "app.widget.view", description: "View widgets" }])
.mockResolvedValueOnce([
{ name: "app.report.view", description: "View reports" },
{ name: "app.report.read", description: "Read reports" },
]);
adminGetAllUsersMock.mockResolvedValue({
items: [
{ id: "admin-user-id", email: "admin@email.com", admin: { userId: "admin-user-id" }, passwordHash: "hashed-password" },
{ id: "other-admin-id", email: "other@example.com", admin: { userId: "other-admin-id" }, token: "sk_live_secret" },
{ id: "app-user-id", email: "app@example.com", admin: null },
] as Awaited<ReturnType<typeof adminGetAllUsers>>["items"],
pagination: {} as Awaited<ReturnType<typeof adminGetAllUsers>>["pagination"],
});
adminGetAllTenantsMock.mockResolvedValue([
{
id: "tenant-1",
users: [
{ userId: "admin-user-id" } as Awaited<ReturnType<typeof adminGetAllTenants>>[number]["users"][number],
{ userId: "app-user-id" } as Awaited<ReturnType<typeof adminGetAllTenants>>[number]["users"][number],
],
} as Awaited<ReturnType<typeof adminGetAllTenants>>[number],
]);
createUserRoleAssignmentsMock.mockResolvedValue({ count: adminRoleRows.length + 2 * appRoleRows.length } as never);
syncDefaultRolesAndPermissionsMock.mockResolvedValue({
roleCount: defaultAdminRoles.length + defaultAppRoles.length,
permissionCount: defaultPermissions.length,
createdPermissions: 0,
createdRolePermissions: 0,
} as Awaited<ReturnType<typeof syncDefaultRolesAndPermissions>>);
});
test("emits bounded start and success counts without credential fields or raw user records", async () => {
const diagnostics = vi.fn();
await seedRolesAndPermissions({ adminEmail: "admin@email.com", diagnostics });
expect(syncDefaultRolesAndPermissionsMock).toHaveBeenCalledOnce();
const events = diagnostics.mock.calls.map(([event]) => event as RolesAndPermissionsSeedDiagnosticEvent);
expect(events).toEqual([
{
phase: "start",
adminRoleCount: defaultAdminRoles.length,
appRoleCount: defaultAppRoles.length,
defaultPermissionCount: defaultPermissions.length,
dynamicEntityPermissionCount: 0,
adminUserAssignmentCount: 0,
tenantUserAssignmentCount: 0,
},
{
phase: "success",
adminRoleCount: adminRoleRows.length,
appRoleCount: appRoleRows.length,
defaultPermissionCount: defaultPermissions.length,
dynamicEntityPermissionCount: 3,
adminUserAssignmentCount: adminRoleRows.length,
tenantUserAssignmentCount: 2 * appRoleRows.length,
},
]);
expect(serializedDiagnostics(events)).not.toContain("password");
expect(serializedDiagnostics(events)).not.toContain("token");
expect(serializedDiagnostics(events)).not.toContain("hashed-password");
expect(serializedDiagnostics(events)).not.toContain("sk_live_secret");
expect(serializedDiagnostics(events)).not.toContain("admin-user-id");
const defaultAppPermissionCount = defaultPermissions.filter((permission) => permission.type === "app").length;
expect(dbMock.permission.createMany).toHaveBeenCalledWith({
data: [
{
order: defaultAppPermissionCount + 3,
name: "app.widget.view",
description: "View widgets",
type: "widget",
isDefault: true,
entityId: null,
},
{
order: defaultAppPermissionCount + 4,
name: "app.report.view",
description: "View reports",
type: "report",
isDefault: true,
entityId: null,
},
{
order: defaultAppPermissionCount + 5,
name: "app.report.read",
description: "Read reports",
type: "report",
isDefault: true,
entityId: null,
},
],
skipDuplicates: true,
});
expect(dbMock.rolePermission.createMany).toHaveBeenCalledWith({
data: expect.arrayContaining([
{ permissionId: "permission-widget-view", roleId: "admin-role-super-admin" },
{ permissionId: "permission-widget-view", roleId: "app-role-super-user" },
{ permissionId: "permission-widget-view", roleId: "app-role-admin" },
{ permissionId: "permission-widget-view", roleId: "app-role-user" },
]),
skipDuplicates: true,
});
expect(clearCacheKeyMock).toHaveBeenCalledWith("permission:app.widget.view");
expect(createUserRoleAssignmentsMock).toHaveBeenCalledWith(
expect.arrayContaining([
{ userId: "admin-user-id", roleId: adminRoleRows[0]?.id, tenantId: null },
{ userId: "admin-user-id", roleId: appRoleRows[0]?.id, tenantId: "tenant-1" },
{ userId: "app-user-id", roleId: appRoleRows[0]?.id, tenantId: "tenant-1" },
])
);
});
test("preserves the legacy adminEmail argument while assigning the seeded admin user", async () => {
await seedRolesAndPermissions("admin@email.com");
expect(createUserRoleAssignmentsMock).toHaveBeenCalledWith(
expect.arrayContaining([{ userId: "admin-user-id", roleId: adminRoleRows[0]?.id, tenantId: null }])
);
});
test("emits failure diagnostics and rethrows the original seed error", async () => {
const diagnostics = vi.fn();
const originalError = new Error("seed failed without password token literals");
getAllEntitiesMock.mockRejectedValueOnce(originalError);
await expect(seedRolesAndPermissions({ adminEmail: "admin@email.com", diagnostics })).rejects.toBe(originalError);
const events = diagnostics.mock.calls.map(([event]) => event as RolesAndPermissionsSeedDiagnosticEvent);
expect(events).toHaveLength(2);
expect(events[1]).toMatchObject({
phase: "failure",
adminRoleCount: adminRoleRows.length,
appRoleCount: appRoleRows.length,
defaultPermissionCount: defaultPermissions.length,
dynamicEntityPermissionCount: 0,
adminUserAssignmentCount: 0,
tenantUserAssignmentCount: 0,
error: originalError,
});
expect(serializedDiagnostics(events)).not.toContain("password");
expect(serializedDiagnostics(events)).not.toContain("token");
expect(serializedDiagnostics(events)).not.toContain("hashed-password");
expect(serializedDiagnostics(events)).not.toContain("sk_live_secret");
});
});