Files
Nexus/packages/api/src/__tests__/role-router-auth.test.ts
T
Hartmut b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
2026-05-21 16:28:40 +02:00

187 lines
6.2 KiB
TypeScript

import { SystemRole } from "@nexus/shared";
import { describe, expect, it, vi } from "vitest";
import { roleRouter } from "../router/role.js";
import { createCallerFactory } from "../trpc.js";
const createCaller = createCallerFactory(roleRouter);
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,
};
}
describe("role router authorization", () => {
describe("unauthenticated access", () => {
it("rejects unauthenticated list call with UNAUTHORIZED", async () => {
const roleFindMany = vi.fn();
const caller = createCaller(
createContext({ role: { findMany: roleFindMany } }, { session: false }),
);
await expect(caller.list({})).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(roleFindMany).not.toHaveBeenCalled();
});
it("rejects unauthenticated create call with UNAUTHORIZED", async () => {
const roleCreate = vi.fn();
const caller = createCaller(
createContext({ role: { create: roleCreate } }, { session: false }),
);
await expect(caller.create({ name: "Art Director" })).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(roleCreate).not.toHaveBeenCalled();
});
});
describe("USER role — insufficient permissions for mutations", () => {
it("forbids USER from calling create", async () => {
const roleCreate = vi.fn();
const caller = createCaller(createContext({ role: { create: roleCreate } }));
await expect(caller.create({ name: "Art Director" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Manager or Admin role required",
});
expect(roleCreate).not.toHaveBeenCalled();
});
it("forbids USER from calling update", async () => {
const roleUpdate = vi.fn();
const caller = createCaller(createContext({ role: { update: roleUpdate } }));
await expect(
caller.update({ id: "role_1", data: { name: "Updated Role" } }),
).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Manager or Admin role required",
});
expect(roleUpdate).not.toHaveBeenCalled();
});
it("forbids USER from calling delete", async () => {
const roleDelete = vi.fn();
const caller = createCaller(createContext({ role: { delete: roleDelete } }));
await expect(caller.delete({ id: "role_1" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Manager or Admin role required",
});
expect(roleDelete).not.toHaveBeenCalled();
});
it("forbids USER from calling deactivate", async () => {
const roleUpdate = vi.fn();
const caller = createCaller(createContext({ role: { update: roleUpdate } }));
await expect(caller.deactivate({ id: "role_1" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Manager or Admin role required",
});
expect(roleUpdate).not.toHaveBeenCalled();
});
});
describe("MANAGER role — permitted for mutations", () => {
it("allows MANAGER to call create without auth error", async () => {
const createdRole = {
id: "role_new",
name: "Art Director",
description: null,
color: null,
isActive: true,
_count: { resourceRoles: 0 },
};
const roleCreate = vi.fn().mockResolvedValue(createdRole);
const roleFindUnique = vi.fn().mockResolvedValue(null); // name not taken
const auditLogCreate = vi.fn().mockResolvedValue({});
// planningEntry count queries (attachZeroAllocationCount path)
const planningEntryFindMany = vi.fn().mockResolvedValue([]);
const db: Record<string, unknown> = {
role: { create: roleCreate, findUnique: roleFindUnique },
auditLog: { create: auditLogCreate },
planningEntry: { findMany: planningEntryFindMany },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createCaller(createContext(db, { role: SystemRole.MANAGER }));
// Should not throw UNAUTHORIZED or FORBIDDEN
const result = await caller.create({ name: "Art Director" });
expect(result).toMatchObject({ id: "role_new", name: "Art Director" });
expect(roleCreate).toHaveBeenCalledTimes(1);
});
});
describe("ADMIN role — permitted for mutations", () => {
it("allows ADMIN to call delete without auth error", async () => {
const existingRole = {
id: "role_1",
name: "Stale Role",
description: null,
color: null,
isActive: true,
_count: { resourceRoles: 0 },
};
const roleFindUnique = vi.fn().mockResolvedValue(existingRole);
const roleDelete = vi.fn().mockResolvedValue(existingRole);
const auditLogCreate = vi.fn().mockResolvedValue({});
// attachSingleRolePlanningEntryCount calls countPlanningEntries
// which needs both demandRequirement and assignment findMany
const demandRequirementFindMany = vi.fn().mockResolvedValue([]);
const assignmentFindMany = vi.fn().mockResolvedValue([]);
const adminDb: Record<string, unknown> = {
role: {
findUnique: roleFindUnique,
delete: roleDelete,
},
auditLog: { create: auditLogCreate },
demandRequirement: { findMany: demandRequirementFindMany },
assignment: { findMany: assignmentFindMany },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(adminDb)),
};
const caller = createCaller(createContext(adminDb, { role: SystemRole.ADMIN }));
const result = await caller.delete({ id: "role_1" });
expect(result).toEqual({ success: true });
expect(roleDelete).toHaveBeenCalledTimes(1);
});
});
});