109 lines
5.2 KiB
JavaScript
109 lines
5.2 KiB
JavaScript
import { addUserContext, invalidateCachesForUser } from "@/middleware/AddUserContext.js";
|
|
import * as Models from "@/db/models/index.js";
|
|
import ApiError from "@/utils/ApiError.js";
|
|
|
|
const mockReq = (user, headers = {}) => ({ user, headers });
|
|
const mockRes = () => ({ });
|
|
const mockNext = () => jest.fn();
|
|
|
|
describe("addUserContext", () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [] }) });
|
|
jest.spyOn(Models.TeamMembership, "findOne").mockReturnValue({ populate: () => ({ select: () => ({ lean: () => ({ roleId: { name: "team-role" } }) }) }) });
|
|
jest.spyOn(Models.OrgMembership, "findOne").mockReturnValue({ populate: () => ({ select: () => ({ lean: () => ({ roleId: { name: "org-role" } }) }) }) });
|
|
});
|
|
afterEach(() => jest.restoreAllMocks());
|
|
|
|
it("throws when user missing", async () => {
|
|
const req = mockReq(undefined, {});
|
|
const res = mockRes();
|
|
const next = mockNext();
|
|
await addUserContext(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
const err = next.mock.calls[0][0];
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
expect(err.status).toBe(401);
|
|
});
|
|
|
|
it("throws when user not in selected team", async () => {
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: "t-2" }] }) });
|
|
const req = mockReq({ sub: "u1", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const res = mockRes();
|
|
const next = mockNext();
|
|
await addUserContext(req, res, next);
|
|
const err = next.mock.calls[0][0];
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
expect(err.status).toBe(403);
|
|
});
|
|
|
|
it("populates roles and proceeds", async () => {
|
|
invalidateCachesForUser("u1");
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
// roles already mocked in beforeEach
|
|
|
|
const req = mockReq({ sub: "u1", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const res = mockRes();
|
|
const next = mockNext();
|
|
await addUserContext(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
expect(req.user.roles.orgRole).toEqual({ name: "org-role" });
|
|
expect(req.user.roles.teamRole).toEqual({ name: "team-role" });
|
|
expect(req.user.currentTeamId).toBe("t-1");
|
|
});
|
|
|
|
it("uses cached teamIds and roles when cache valid", async () => {
|
|
// First call populates cache
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
const req1 = mockReq({ sub: "u-cache", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const next1 = mockNext();
|
|
await addUserContext(req1, mockRes(), next1);
|
|
expect(next1).toHaveBeenCalled();
|
|
|
|
// Second call should hit cache and still succeed; mock find to fail if called
|
|
jest.spyOn(Models.TeamMembership, "find").mockImplementation(() => { throw new Error("should not query"); });
|
|
const req2 = mockReq({ sub: "u-cache", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const next2 = mockNext();
|
|
await addUserContext(req2, mockRes(), next2);
|
|
expect(next2).toHaveBeenCalled();
|
|
});
|
|
|
|
it("invalidates caches for user", async () => {
|
|
// Warm caches
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
const req = mockReq({ sub: "u-inv", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const next = mockNext();
|
|
await addUserContext(req, mockRes(), next);
|
|
expect(next).toHaveBeenCalled();
|
|
|
|
// Invalidate and ensure it re-queries without throwing
|
|
invalidateCachesForUser("u-inv");
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
const next2 = mockNext();
|
|
await addUserContext(mockReq({ sub: "u-inv", orgId: "o1" }, { "x-team-id": "t-1" }), mockRes(), next2);
|
|
expect(next2).toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws when org role missing", async () => {
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
jest.spyOn(Models.OrgMembership, "findOne").mockReturnValue({ populate: () => ({ select: () => ({ lean: () => null }) }) });
|
|
const req = mockReq({ sub: "u2", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const next = mockNext();
|
|
await addUserContext(req, mockRes(), next);
|
|
const err = next.mock.calls[0][0];
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
expect(err.status).toBe(403);
|
|
});
|
|
|
|
it("throws when team role missing", async () => {
|
|
jest.spyOn(Models.TeamMembership, "find").mockReturnValue({ select: () => ({ lean: () => [{ teamId: { toString: () => "t-1" } }] }) });
|
|
jest.spyOn(Models.OrgMembership, "findOne").mockReturnValue({ populate: () => ({ select: () => ({ lean: () => ({ roleId: { name: "org-role" } }) }) }) });
|
|
jest.spyOn(Models.TeamMembership, "findOne").mockReturnValue({ populate: () => ({ select: () => ({ lean: () => null }) }) });
|
|
const req = mockReq({ sub: "u3", orgId: "o1" }, { "x-team-id": "t-1" });
|
|
const next = mockNext();
|
|
await addUserContext(req, mockRes(), next);
|
|
const err = next.mock.calls[0][0];
|
|
expect(err).toBeInstanceOf(ApiError);
|
|
expect(err.status).toBe(403);
|
|
});
|
|
});
|