diff --git a/app/api/UserLab/__tests__/route.test.ts b/app/api/UserLab/__tests__/route.test.ts index 2f10795..aa71912 100644 --- a/app/api/UserLab/__tests__/route.test.ts +++ b/app/api/UserLab/__tests__/route.test.ts @@ -36,7 +36,9 @@ describe("UserLab API route", () => { } as never); const response = await GET( - new Request("http://localhost/api/UserLab?page=2&limit=10") + new Request( + "http://localhost/api/UserLab?page=2&limit=10&labId=507f1f77bcf86cd799439012" + ) ); expect(response.status).toBe(200); @@ -47,12 +49,18 @@ describe("UserLab API route", () => { total: 11, totalPages: 2, }); - expect(mockedGetUserLabs).toHaveBeenCalledWith({ page: 2, limit: 10 }); + expect(mockedGetUserLabs).toHaveBeenCalledWith({ + page: 2, + limit: 10, + labId: "507f1f77bcf86cd799439012", + }); }); it("returns 400 when pagination exceeds the allowed limit", async () => { const response = await GET( - new Request("http://localhost/api/UserLab?limit=20") + new Request( + "http://localhost/api/UserLab?limit=20&labId=507f1f77bcf86cd799439012" + ) ); expect(response.status).toBe(400); @@ -61,6 +69,17 @@ describe("UserLab API route", () => { }); }); + it("returns 400 when the list request is missing a labId", async () => { + const response = await GET( + new Request("http://localhost/api/UserLab?page=1&limit=10") + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toEqual({ + message: "Invalid input: expected string, received undefined", + }); + }); + it("returns 404 when a requested entry does not exist", async () => { mockedGetUserLab.mockResolvedValue(null as never); @@ -119,7 +138,9 @@ describe("UserLab API route", () => { mockedGetUserLabs.mockRejectedValue(new Error("Unauthorized: Insufficient permissions")); const response = await GET( - new Request("http://localhost/api/UserLab?page=1&limit=10") + new Request( + "http://localhost/api/UserLab?page=1&limit=10&labId=507f1f77bcf86cd799439012" + ) ); expect(response.status).toBe(403); diff --git a/app/api/UserLab/route.ts b/app/api/UserLab/route.ts index 637fef2..b6166e0 100644 --- a/app/api/UserLab/route.ts +++ b/app/api/UserLab/route.ts @@ -17,6 +17,7 @@ const objectIdSchema = z const paginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(10).default(10), + labId: objectIdSchema, }); const userLabBaseSchema = z.object({ @@ -74,6 +75,7 @@ export async function GET(request: Request) { const pagination = paginationSchema.parse({ page: searchParams.get("page") ?? undefined, limit: searchParams.get("limit") ?? undefined, + labId: searchParams.get("labId") ?? undefined, }); const entries = await getUserLabs(pagination); return NextResponse.json(entries, { status: 200 }); diff --git a/services/UserLab.ts b/services/UserLab.ts index 920daeb..02bd6f9 100644 --- a/services/UserLab.ts +++ b/services/UserLab.ts @@ -6,6 +6,7 @@ import UserLab from "@/models/UserLab"; export type GetUserLabsOptions = { page: number; limit: number; + labId: string; }; export type UserLabPayload = Pick; @@ -18,14 +19,27 @@ async function requireUserLabPermission() { } } -export async function getUserLabs({ page, limit }: GetUserLabsOptions) { - await requireUserLabPermission(); +export async function getUserLabs({ page, limit, labId }: GetUserLabsOptions) { + const { allowed, user, reason } = await getSession("lab:manage_users"); + + if (!allowed) { + throw new Error(`Unauthorized: ${reason}`); + } + + const belongsToLab = user?.labs?.some( + (membership) => String(membership.labId) === labId + ); + + if (!belongsToLab) { + throw new Error("Unauthorized: Not a member of this lab"); + } + await connectToDatabase(); const skip = (page - 1) * limit; const [items, total] = await Promise.all([ - UserLab.find().skip(skip).limit(limit).populate("user").populate("lab"), - UserLab.countDocuments(), + UserLab.find({ lab: labId }).skip(skip).limit(limit).populate("user").populate("lab"), + UserLab.countDocuments({ lab: labId }), ]); return { diff --git a/services/__tests__/UserLab.test.ts b/services/__tests__/UserLab.test.ts index 03bef7e..7eb2e85 100644 --- a/services/__tests__/UserLab.test.ts +++ b/services/__tests__/UserLab.test.ts @@ -39,7 +39,10 @@ describe("UserLab service", () => { mockedConnectToDatabase.mockResolvedValue({} as never); mockedGetSession.mockResolvedValue({ allowed: true, - user: { role: "PI" }, + user: { + role: "PI", + labs: [{ labId: "507f1f77bcf86cd799439012", role: "PI" }], + }, reason: undefined, } as never); }); @@ -54,16 +57,24 @@ describe("UserLab service", () => { mockedUserLabModel.find.mockReturnValue({ skip } as never); mockedUserLabModel.countDocuments.mockResolvedValue(12 as never); - const result = await getUserLabs({ page: 2, limit: 5 }); + const result = await getUserLabs({ + page: 2, + limit: 5, + labId: "507f1f77bcf86cd799439012", + }); expect(mockedGetSession).toHaveBeenCalledWith("lab:manage_users"); expect(connectToDatabase).toHaveBeenCalledTimes(1); - expect(mockedUserLabModel.find).toHaveBeenCalledTimes(1); + expect(mockedUserLabModel.find).toHaveBeenCalledWith({ + lab: "507f1f77bcf86cd799439012", + }); expect(skip).toHaveBeenCalledWith(5); expect(limit).toHaveBeenCalledWith(5); expect(populateUser).toHaveBeenCalledWith("user"); expect(populateLab).toHaveBeenCalledWith("lab"); - expect(mockedUserLabModel.countDocuments).toHaveBeenCalledTimes(1); + expect(mockedUserLabModel.countDocuments).toHaveBeenCalledWith({ + lab: "507f1f77bcf86cd799439012", + }); expect(result).toEqual({ items: [{ _id: "1" }, { _id: "2" }], page: 2, @@ -83,7 +94,11 @@ describe("UserLab service", () => { mockedUserLabModel.find.mockReturnValue({ skip } as never); mockedUserLabModel.countDocuments.mockResolvedValue(0 as never); - const result = await getUserLabs({ page: 1, limit: 10 }); + const result = await getUserLabs({ + page: 1, + limit: 10, + labId: "507f1f77bcf86cd799439012", + }); expect(result).toEqual({ items: [], @@ -101,10 +116,27 @@ describe("UserLab service", () => { reason: "Insufficient permissions", } as never); - await expect(getUserLabs({ page: 1, limit: 10 })).rejects.toThrow( - "Unauthorized: Insufficient permissions" - ); + await expect( + getUserLabs({ + page: 1, + limit: 10, + labId: "507f1f77bcf86cd799439012", + }) + ).rejects.toThrow("Unauthorized: Insufficient permissions"); + expect(mockedConnectToDatabase).not.toHaveBeenCalled(); + }); + + it("throws when the current user does not belong to the requested lab", async () => { + await expect( + getUserLabs({ + page: 1, + limit: 10, + labId: "507f1f77bcf86cd799439099", + }) + ).rejects.toThrow("Unauthorized: Not a member of this lab"); + expect(mockedConnectToDatabase).not.toHaveBeenCalled(); + expect(mockedUserLabModel.find).not.toHaveBeenCalled(); }); it("gets one user lab by id", async () => {