134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../db.server", () => ({
|
|
db: {
|
|
group: {
|
|
create: vi.fn(),
|
|
delete: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
findMany: vi.fn(),
|
|
update: vi.fn(),
|
|
updateMany: vi.fn(),
|
|
},
|
|
groupUser: {
|
|
createMany: vi.fn(),
|
|
deleteMany: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
import { db } from "../../db.server";
|
|
import { addUsersToGroup, createGroup, deleteGroup, getAllGroups, getGroup, getGroups, getMyGroups, removeUsersFromGroup, updateGroup } from "./groups.db.server";
|
|
|
|
const dbMock = vi.mocked(db);
|
|
|
|
describe("group persistence", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("lists tenant groups and selected groups with users included", async () => {
|
|
vi.mocked(dbMock.group.findMany).mockResolvedValue([] as never);
|
|
|
|
await getAllGroups("tenant_1");
|
|
await getGroups("tenant_1", ["group_1", "group_2"]);
|
|
|
|
expect(dbMock.group.findMany).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
where: { tenantId: "tenant_1" },
|
|
include: expect.objectContaining({ users: expect.any(Object) }),
|
|
orderBy: [{ name: "asc" }],
|
|
})
|
|
);
|
|
expect(dbMock.group.findMany).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
where: { tenantId: "tenant_1", id: { in: ["group_1", "group_2"] } },
|
|
include: expect.objectContaining({ users: expect.any(Object) }),
|
|
orderBy: [{ name: "asc" }],
|
|
})
|
|
);
|
|
});
|
|
|
|
it("limits my-groups lookup to created or joined groups in the tenant", async () => {
|
|
vi.mocked(dbMock.group.findMany).mockResolvedValueOnce([] as never);
|
|
|
|
await getMyGroups("user_1", "tenant_1");
|
|
|
|
expect(dbMock.group.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: {
|
|
tenantId: "tenant_1",
|
|
OR: [{ createdByUserId: "user_1" }, { users: { some: { userId: "user_1" } } }],
|
|
},
|
|
include: expect.objectContaining({ users: expect.any(Object) }),
|
|
orderBy: [{ name: "asc" }],
|
|
})
|
|
);
|
|
});
|
|
|
|
it("gets groups with optional tenant scope", async () => {
|
|
vi.mocked(dbMock.group.findFirst).mockResolvedValue({ id: "group_1" } as never);
|
|
|
|
await getGroup("group_1");
|
|
await getGroup("group_1", "tenant_1");
|
|
|
|
expect(dbMock.group.findFirst).toHaveBeenNthCalledWith(1, expect.objectContaining({ where: { id: "group_1" } }));
|
|
expect(dbMock.group.findFirst).toHaveBeenNthCalledWith(2, expect.objectContaining({ where: { id: "group_1", tenantId: "tenant_1" } }));
|
|
});
|
|
|
|
it("creates and mutates groups with tenant-scoped bulk operations when a tenant is provided", async () => {
|
|
vi.mocked(dbMock.group.create).mockResolvedValueOnce({ id: "group_1" } as never);
|
|
vi.mocked(dbMock.group.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
|
vi.mocked(dbMock.group.update).mockResolvedValueOnce({ id: "group_1" } as never);
|
|
vi.mocked(dbMock.group.deleteMany).mockResolvedValueOnce({ count: 1 } as never);
|
|
vi.mocked(dbMock.group.delete).mockResolvedValueOnce({ id: "group_1" } as never);
|
|
|
|
const data = { createdByUserId: "user_1", tenantId: "tenant_1", name: "Team", description: "Core team", color: 1 };
|
|
await createGroup(data);
|
|
await updateGroup("group_1", { name: "Ops", description: "Ops team", color: 2 }, "tenant_1");
|
|
await updateGroup("group_1", { name: "Ops", description: "Ops team", color: 2 });
|
|
await deleteGroup("group_1", "tenant_1");
|
|
await deleteGroup("group_1");
|
|
|
|
expect(dbMock.group.create).toHaveBeenCalledWith({ data });
|
|
expect(dbMock.group.updateMany).toHaveBeenCalledWith({
|
|
where: { id: "group_1", tenantId: "tenant_1" },
|
|
data: { name: "Ops", description: "Ops team", color: 2 },
|
|
});
|
|
expect(dbMock.group.update).toHaveBeenCalledWith({
|
|
where: { id: "group_1" },
|
|
data: { name: "Ops", description: "Ops team", color: 2 },
|
|
});
|
|
expect(dbMock.group.deleteMany).toHaveBeenCalledWith({ where: { id: "group_1", tenantId: "tenant_1" } });
|
|
expect(dbMock.group.delete).toHaveBeenCalledWith({ where: { id: "group_1" } });
|
|
});
|
|
|
|
it("adds users to a group idempotently (skipDuplicates) and removes them in bulk", async () => {
|
|
vi.mocked(dbMock.groupUser.createMany).mockResolvedValueOnce({ count: 2 } as never);
|
|
vi.mocked(dbMock.groupUser.deleteMany).mockResolvedValueOnce({ count: 1 } as never);
|
|
|
|
await addUsersToGroup("group_1", ["user_1", "user_2"]);
|
|
await removeUsersFromGroup("group_1", ["user_1"]);
|
|
|
|
expect(dbMock.groupUser.createMany).toHaveBeenCalledWith({
|
|
data: [
|
|
{ groupId: "group_1", userId: "user_1" },
|
|
{ groupId: "group_1", userId: "user_2" },
|
|
],
|
|
skipDuplicates: true,
|
|
});
|
|
expect(dbMock.groupUser.deleteMany).toHaveBeenCalledWith({ where: { groupId: "group_1", userId: { in: ["user_1"] } } });
|
|
});
|
|
|
|
it("short-circuits bulk membership ops on an empty user list", async () => {
|
|
expect(await addUsersToGroup("group_1", [])).toEqual({ count: 0 });
|
|
expect(await removeUsersFromGroup("group_1", [])).toEqual({ count: 0 });
|
|
expect(dbMock.groupUser.createMany).not.toHaveBeenCalled();
|
|
expect(dbMock.groupUser.deleteMany).not.toHaveBeenCalled();
|
|
});
|
|
});
|