feat(org-unit): scope structural reads to resource overview
This commit is contained in:
@@ -649,6 +649,7 @@ describe("assistant router tool gating", () => {
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
const userWithResourceOverview = getToolNames([PermissionKey.VIEW_ALL_RESOURCES], SystemRole.USER);
|
||||
const userWithManagedResources = getToolNames([PermissionKey.MANAGE_RESOURCES], SystemRole.USER);
|
||||
|
||||
expect(adminNames).toContain("list_countries");
|
||||
expect(adminNames).toContain("create_country");
|
||||
@@ -668,6 +669,9 @@ describe("assistant router tool gating", () => {
|
||||
expect(userWithResourceOverview).toContain("search_resources");
|
||||
expect(userWithResourceOverview).toContain("get_country");
|
||||
expect(userWithResourceOverview).toContain("list_org_units");
|
||||
expect(userWithManagedResources).toContain("search_resources");
|
||||
expect(userWithManagedResources).toContain("get_country");
|
||||
expect(userWithManagedResources).toContain("list_org_units");
|
||||
});
|
||||
|
||||
it("blocks mutation tools until the user confirms a prior assistant summary", () => {
|
||||
|
||||
@@ -23,6 +23,33 @@ function createProtectedContext(
|
||||
}
|
||||
|
||||
describe("master-data router authorization", () => {
|
||||
it("keeps country lists available to authenticated users as safe lookup data", async () => {
|
||||
const findMany = vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "country_de",
|
||||
code: "DE",
|
||||
name: "Germany",
|
||||
isActive: true,
|
||||
dailyWorkingHours: 8,
|
||||
metroCities: [{ id: "city_ber", name: "Berlin", countryId: "country_de" }],
|
||||
},
|
||||
]);
|
||||
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
||||
country: {
|
||||
findMany,
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await caller.list({ isActive: true });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { isActive: true },
|
||||
include: { metroCities: { orderBy: { name: "asc" } } },
|
||||
orderBy: { name: "asc" },
|
||||
}));
|
||||
});
|
||||
|
||||
it("keeps minimal country lookups available to authenticated users", async () => {
|
||||
const findFirst = vi.fn().mockResolvedValue({
|
||||
id: "country_de",
|
||||
@@ -50,6 +77,31 @@ describe("master-data router authorization", () => {
|
||||
expect(findFirst).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps metro-city lookups available to authenticated users", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "city_muc",
|
||||
name: "Munich",
|
||||
countryId: "country_de",
|
||||
});
|
||||
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
||||
metroCity: {
|
||||
findUnique,
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await caller.getCityById({ id: "city_muc" });
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "city_muc",
|
||||
name: "Munich",
|
||||
countryId: "country_de",
|
||||
});
|
||||
expect(findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "city_muc" },
|
||||
select: { id: true, name: true, countryId: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("requires resource overview access for detailed country reads with resource counts", async () => {
|
||||
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
||||
country: {
|
||||
@@ -99,6 +151,31 @@ describe("master-data router authorization", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("allows detailed country reads for users with manage-resources access", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "country_de",
|
||||
code: "DE",
|
||||
name: "Germany",
|
||||
isActive: true,
|
||||
dailyWorkingHours: 8,
|
||||
scheduleRules: null,
|
||||
metroCities: [],
|
||||
_count: { resources: 4 },
|
||||
});
|
||||
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
||||
country: {
|
||||
findUnique,
|
||||
},
|
||||
}, {
|
||||
granted: [PermissionKey.MANAGE_RESOURCES],
|
||||
}));
|
||||
|
||||
const result = await caller.getById({ id: "country_de" });
|
||||
|
||||
expect(result._count.resources).toBe(4);
|
||||
expect(findUnique).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps minimal org-unit lookups available to authenticated users", async () => {
|
||||
const findFirst = vi.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
@@ -127,6 +204,26 @@ describe("master-data router authorization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires resource overview access for org-unit list and tree reads", async () => {
|
||||
const findMany = vi.fn();
|
||||
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
||||
orgUnit: {
|
||||
findMany,
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(caller.list({ level: 5, isActive: true })).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
message: "Resource overview access required",
|
||||
});
|
||||
await expect(caller.getTree({ isActive: true })).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
message: "Resource overview access required",
|
||||
});
|
||||
|
||||
expect(findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires resource overview access for detailed org-unit reads with staffing counts", async () => {
|
||||
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
||||
orgUnit: {
|
||||
@@ -172,4 +269,62 @@ describe("master-data router authorization", () => {
|
||||
include: { _count: { select: { resources: true } } },
|
||||
}));
|
||||
});
|
||||
|
||||
it("allows org-unit list and tree reads for users with resource overview access", async () => {
|
||||
const listFindMany = vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "ou_1",
|
||||
name: "Delivery",
|
||||
shortName: "DEL",
|
||||
level: 5,
|
||||
parentId: null,
|
||||
sortOrder: 10,
|
||||
isActive: true,
|
||||
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
const treeFindMany = vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "ou_1",
|
||||
name: "Delivery",
|
||||
shortName: "DEL",
|
||||
level: 5,
|
||||
parentId: null,
|
||||
sortOrder: 10,
|
||||
isActive: true,
|
||||
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
||||
orgUnit: {
|
||||
findMany: vi.fn()
|
||||
.mockImplementationOnce(listFindMany)
|
||||
.mockImplementationOnce(treeFindMany),
|
||||
},
|
||||
}, {
|
||||
granted: [PermissionKey.MANAGE_RESOURCES],
|
||||
}));
|
||||
|
||||
const listResult = await caller.list({ level: 5, isActive: true });
|
||||
const treeResult = await caller.getTree({ isActive: true });
|
||||
|
||||
expect(listResult).toHaveLength(1);
|
||||
expect(treeResult).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "ou_1",
|
||||
name: "Delivery",
|
||||
children: [],
|
||||
}),
|
||||
]);
|
||||
expect(listFindMany).toHaveBeenCalledWith({
|
||||
where: { level: 5, isActive: true },
|
||||
orderBy: [{ level: "asc" }, { sortOrder: "asc" }, { name: "asc" }],
|
||||
});
|
||||
expect(treeFindMany).toHaveBeenCalledWith({
|
||||
where: { isActive: true },
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ function buildTree(flatItems: FlatOrgUnit[], parentId: string | null = null): Or
|
||||
}
|
||||
|
||||
export const orgUnitRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
list: resourceOverviewProcedure
|
||||
.input(
|
||||
z.object({
|
||||
level: z.number().int().min(5).max(7).optional(),
|
||||
@@ -54,7 +54,7 @@ export const orgUnitRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
getTree: protectedProcedure
|
||||
getTree: resourceOverviewProcedure
|
||||
.input(z.object({ isActive: z.boolean().optional() }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const all = await ctx.db.orgUnit.findMany({
|
||||
|
||||
Reference in New Issue
Block a user