253 lines
11 KiB
TypeScript
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");
|
|
});
|
|
});
|