Files
CapaKraken/packages/api/src/__tests__/user-router-auth.test.ts
T

355 lines
9.5 KiB
TypeScript

import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { userRouter } from "../router/user.js";
import { createCallerFactory } from "../trpc.js";
const createCaller = createCallerFactory(userRouter);
function createContext(
db: Record<string, unknown>,
options: {
role?: SystemRole;
session?: boolean;
} = {},
) {
const { role = SystemRole.USER, session = true } = options;
return {
session: session
? {
user: { email: "user@example.com", name: "User", image: null },
expires: "2099-01-01T00:00:00.000Z",
}
: null,
db: db as never,
dbUser: session
? {
id: role === SystemRole.ADMIN ? "user_admin" : "user_1",
systemRole: role,
permissionOverrides: null,
}
: null,
roleDefaults: null,
};
}
describe("user router authorization", () => {
it("requires authentication for self-service profile lookups", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.me()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("requires authentication for dashboard layout reads", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.getDashboardLayout()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("requires authentication for favorite project toggles", async () => {
const findUnique = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
update,
},
}, { session: false }));
await expect(caller.toggleFavoriteProject({ projectId: "project_1" })).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("requires authentication for dashboard layout saves", async () => {
const update = vi.fn();
const caller = createCaller(createContext({
user: {
update,
},
}, { session: false }));
await expect(caller.saveDashboardLayout({
layout: { version: 2, gridCols: 12, widgets: [] },
})).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(update).not.toHaveBeenCalled();
});
it("requires authentication for favorite project reads", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.getFavoriteProjectIds()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("requires authentication for column preference reads and writes", async () => {
const findUnique = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
update,
},
}, { session: false }));
await expect(caller.getColumnPreferences()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
await expect(caller.setColumnPreferences({
view: "resources",
visible: ["name"],
})).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("requires authentication for MFA status lookups", async () => {
const findUniqueOrThrow = vi.fn();
const caller = createCaller(createContext({
user: {
findUniqueOrThrow,
},
}, { session: false }));
await expect(caller.getMfaStatus()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUniqueOrThrow).not.toHaveBeenCalled();
});
it("forbids regular users from listing assignable users", async () => {
const findMany = vi.fn();
const caller = createCaller(createContext({
user: {
findMany,
},
}));
await expect(caller.listAssignable()).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Manager or Admin role required",
});
expect(findMany).not.toHaveBeenCalled();
});
it("allows managers to list assignable users", async () => {
const findMany = vi.fn().mockResolvedValue([
{ id: "user_1", name: "Alice", email: "alice@example.com" },
]);
const caller = createCaller(createContext({
user: {
findMany,
},
}, { role: SystemRole.MANAGER }));
const result = await caller.listAssignable();
expect(result).toHaveLength(1);
expect(findMany).toHaveBeenCalledWith({
select: {
id: true,
name: true,
email: true,
},
orderBy: { name: "asc" },
});
});
it("forbids non-admin users from reading effective permissions", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.getEffectivePermissions({ userId: "user_2" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("forbids non-admin users from setting explicit permission overrides", async () => {
const findUnique = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
update,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.setPermissions({
userId: "user_2",
overrides: {
granted: ["manageProjects"],
},
})).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(findUnique).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("forbids non-admin users from resetting permission overrides", async () => {
const findUnique = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
update,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.resetPermissions({ userId: "user_2" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(findUnique).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("forbids non-admin users from disabling TOTP for other users", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.disableTotp({ userId: "user_2" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("forbids non-admin users from linking resources", async () => {
const userFindUnique = vi.fn();
const resourceFindUnique = vi.fn();
const updateMany = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique: userFindUnique,
},
resource: {
findUnique: resourceFindUnique,
updateMany,
update,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.linkResource({
userId: "user_2",
resourceId: "resource_1",
})).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(userFindUnique).not.toHaveBeenCalled();
expect(resourceFindUnique).not.toHaveBeenCalled();
expect(updateMany).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("forbids non-admin users from auto-linking users by email", async () => {
const userFindMany = vi.fn();
const resourceFindFirst = vi.fn();
const resourceUpdate = vi.fn();
const caller = createCaller(createContext({
user: {
findMany: userFindMany,
},
resource: {
findFirst: resourceFindFirst,
update: resourceUpdate,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.autoLinkAllByEmail()).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(userFindMany).not.toHaveBeenCalled();
expect(resourceFindFirst).not.toHaveBeenCalled();
expect(resourceUpdate).not.toHaveBeenCalled();
});
it("keeps TOTP verification public for the login flow", async () => {
const findUnique = vi.fn().mockResolvedValue({
id: "user_1",
totpEnabled: false,
totpSecret: null,
});
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.verifyTotp({ userId: "user_1", token: "123456" })).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "TOTP is not enabled for this user.",
});
expect(findUnique).toHaveBeenCalledWith({
where: { id: "user_1" },
select: { id: true, totpSecret: true, totpEnabled: true },
});
});
});