refactor(api): extract user procedures
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import { resolvePermissions, SystemRole } from "@capakraken/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
countActiveUsers,
|
||||
getEffectiveUserPermissions,
|
||||
linkUserResource,
|
||||
listAssignableUsers,
|
||||
} from "../router/user-procedure-support.js";
|
||||
import {
|
||||
getCurrentMfaStatus,
|
||||
getCurrentUserProfile,
|
||||
getDashboardLayout,
|
||||
setColumnPreferences,
|
||||
toggleFavoriteProject,
|
||||
} from "../router/user-self-service-procedure-support.js";
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({
|
||||
createAuditEntry: vi.fn(),
|
||||
}));
|
||||
|
||||
function createContext(db: Record<string, unknown>, overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
db: db as never,
|
||||
dbUser: {
|
||||
id: "user_admin",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
session: {
|
||||
user: { email: "admin@example.com", name: "Admin", image: null },
|
||||
expires: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("user-procedure-support", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("lists assignable users with the expected lightweight selection", async () => {
|
||||
const findMany = vi.fn().mockResolvedValue([
|
||||
{ id: "user_1", name: "Alice", email: "alice@example.com" },
|
||||
]);
|
||||
|
||||
const result = await listAssignableUsers(createContext({
|
||||
user: { findMany },
|
||||
}));
|
||||
|
||||
expect(result).toEqual([{ id: "user_1", name: "Alice", email: "alice@example.com" }]);
|
||||
expect(findMany).toHaveBeenCalledWith({
|
||||
select: { id: true, name: true, email: true },
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
});
|
||||
|
||||
it("counts only users active within the trailing five minute window", async () => {
|
||||
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(new Date("2026-03-30T20:00:00.000Z").valueOf());
|
||||
const count = vi.fn().mockResolvedValue(4);
|
||||
|
||||
const result = await countActiveUsers(createContext({
|
||||
user: { count },
|
||||
}));
|
||||
|
||||
expect(result).toEqual({ count: 4 });
|
||||
expect(count).toHaveBeenCalledWith({
|
||||
where: { lastActiveAt: { gte: new Date("2026-03-30T19:55:00.000Z") } },
|
||||
});
|
||||
nowSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("reads the current user profile by db user id", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "user_admin",
|
||||
name: "Admin",
|
||||
email: "admin@example.com",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
createdAt: new Date("2026-03-30T08:00:00.000Z"),
|
||||
});
|
||||
|
||||
const result = await getCurrentUserProfile(createContext({
|
||||
user: { findUnique },
|
||||
}));
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "user_admin",
|
||||
name: "Admin",
|
||||
email: "admin@example.com",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
createdAt: new Date("2026-03-30T08:00:00.000Z"),
|
||||
});
|
||||
expect(findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user_admin" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
systemRole: true,
|
||||
permissionOverrides: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("unlinks an existing resource before linking the requested one", async () => {
|
||||
const userFindUnique = vi.fn().mockResolvedValue({ id: "user_1" });
|
||||
const resourceFindUnique = vi.fn().mockResolvedValue({ id: "resource_1", userId: null });
|
||||
const updateMany = vi.fn()
|
||||
.mockResolvedValueOnce({ count: 1 })
|
||||
.mockResolvedValueOnce({ count: 1 });
|
||||
|
||||
const result = await linkUserResource(createContext({
|
||||
user: { findUnique: userFindUnique },
|
||||
resource: { findUnique: resourceFindUnique, updateMany },
|
||||
}), {
|
||||
userId: "user_1",
|
||||
resourceId: "resource_1",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(updateMany).toHaveBeenNthCalledWith(1, {
|
||||
where: { userId: "user_1", NOT: { id: "resource_1" } },
|
||||
data: { userId: null },
|
||||
});
|
||||
expect(updateMany).toHaveBeenNthCalledWith(2, {
|
||||
where: {
|
||||
id: "resource_1",
|
||||
OR: [{ userId: null }, { userId: "user_1" }],
|
||||
},
|
||||
data: { userId: "user_1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes dashboard layouts before returning them", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
dashboardLayout: {
|
||||
widgets: [{ id: "peakTimes", position: { x: 0, y: 0, w: 4, h: 3 } }],
|
||||
},
|
||||
updatedAt: new Date("2026-03-30T18:00:00.000Z"),
|
||||
});
|
||||
|
||||
const result = await getDashboardLayout(createContext({
|
||||
user: { findUnique },
|
||||
}));
|
||||
|
||||
expect(result).toEqual({
|
||||
layout: { version: 2, gridCols: 12, widgets: [] },
|
||||
updatedAt: new Date("2026-03-30T18:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles favorite projects without losing the existing list", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
favoriteProjectIds: ["project_1"],
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({});
|
||||
|
||||
const result = await toggleFavoriteProject(createContext({
|
||||
user: { findUnique, update },
|
||||
}), {
|
||||
projectId: "project_2",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
favoriteProjectIds: ["project_1", "project_2"],
|
||||
added: true,
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "user_admin" },
|
||||
data: { favoriteProjectIds: ["project_1", "project_2"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("merges column preferences while preserving untouched sort and row order", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
columnPreferences: {
|
||||
resources: {
|
||||
visible: ["name", "role"],
|
||||
sort: { field: "name", dir: "asc" },
|
||||
rowOrder: ["name", "role", "email"],
|
||||
},
|
||||
},
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({ id: "user_admin" });
|
||||
|
||||
const result = await setColumnPreferences(createContext({
|
||||
user: { findUnique, update },
|
||||
}), {
|
||||
view: "resources",
|
||||
visible: ["name", "email"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "user_admin" },
|
||||
data: {
|
||||
columnPreferences: {
|
||||
resources: {
|
||||
visible: ["name", "email"],
|
||||
sort: { field: "name", dir: "asc" },
|
||||
rowOrder: ["name", "role", "email"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns effective permissions alongside stored overrides", async () => {
|
||||
const overrides = {
|
||||
granted: ["manageProjects"],
|
||||
denied: ["viewCosts"],
|
||||
chapterIds: ["chapter_design"],
|
||||
};
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
systemRole: SystemRole.MANAGER,
|
||||
permissionOverrides: overrides,
|
||||
});
|
||||
|
||||
const result = await getEffectiveUserPermissions(createContext({
|
||||
user: { findUnique },
|
||||
}), {
|
||||
userId: "user_2",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
systemRole: SystemRole.MANAGER,
|
||||
effectivePermissions: Array.from(resolvePermissions(SystemRole.MANAGER, overrides)),
|
||||
overrides,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports MFA status for the current user and throws when the user no longer exists", async () => {
|
||||
const findUnique = vi.fn()
|
||||
.mockResolvedValueOnce({ totpEnabled: true })
|
||||
.mockResolvedValueOnce(null);
|
||||
const ctx = createContext({
|
||||
user: { findUnique },
|
||||
});
|
||||
|
||||
await expect(getCurrentMfaStatus(ctx)).resolves.toEqual({ totpEnabled: true });
|
||||
await expect(getCurrentMfaStatus(ctx)).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user