Files
Checkmate/server/tests/middleware/AddUserContext.test.js
T
2025-11-24 22:49:55 +00:00

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);
});
});