Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/tenantUserLifecycle.server.test.ts
T

79 lines
4.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
import { TenantUserType } from "~/application/enums/tenants/TenantUserType";
const dbMock = vi.hoisted(() => ({
tenantUser: { findFirst: vi.fn(), update: vi.fn(), count: vi.fn() },
group: { updateMany: vi.fn() },
}));
vi.mock("~/utils/db.server", () => ({ db: dbMock, dbRead: dbMock }));
import { deactivateTenantUser, reactivateTenantUser } from "./tenantUserLifecycle.server";
beforeEach(() => {
vi.clearAllMocks();
dbMock.tenantUser.update.mockResolvedValue({});
dbMock.group.updateMany.mockResolvedValue({ count: 0 });
});
describe("deactivateTenantUser", () => {
it("returns not-found when the member does not exist in the tenant", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue(null);
expect(await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: false, reason: "not-found" });
expect(dbMock.tenantUser.update).not.toHaveBeenCalled();
});
it("is idempotent for an already inactive member", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.MEMBER, status: TenantUserStatus.INACTIVE });
expect(await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: false, reason: "already-inactive" });
expect(dbMock.tenantUser.update).not.toHaveBeenCalled();
});
it("blocks deactivating the last active admin/owner", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.ADMIN, status: TenantUserStatus.ACTIVE });
dbMock.tenantUser.count.mockResolvedValue(0); // no other active privileged member
expect(await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: false, reason: "last-admin" });
expect(dbMock.tenantUser.update).not.toHaveBeenCalled();
});
it("deactivates an admin when another active admin remains", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.OWNER, status: TenantUserStatus.ACTIVE });
dbMock.tenantUser.count.mockResolvedValue(1);
expect(await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: true });
expect(dbMock.tenantUser.update).toHaveBeenCalledWith({ where: { id: "m1" }, data: { status: TenantUserStatus.INACTIVE } });
});
it("deactivates a plain member without an admin-count check", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.MEMBER, status: TenantUserStatus.ACTIVE });
expect(await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: true });
expect(dbMock.tenantUser.count).not.toHaveBeenCalled();
});
it("reassigns owned groups to the successor before deactivating", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.MEMBER, status: TenantUserStatus.ACTIVE });
dbMock.group.updateMany.mockResolvedValue({ count: 2 });
await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1", reassignToUserId: "u2" });
expect(dbMock.group.updateMany).toHaveBeenCalledWith({ where: { tenantId: "t1", createdByUserId: "u1" }, data: { createdByUserId: "u2" } });
});
it("does not reassign to the same user", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", type: TenantUserType.MEMBER, status: TenantUserStatus.ACTIVE });
await deactivateTenantUser({ tenantId: "t1", targetUserId: "u1", reassignToUserId: "u1" });
expect(dbMock.group.updateMany).not.toHaveBeenCalled();
});
});
describe("reactivateTenantUser", () => {
it("reactivates an inactive member", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", status: TenantUserStatus.INACTIVE });
expect(await reactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: true });
expect(dbMock.tenantUser.update).toHaveBeenCalledWith({ where: { id: "m1" }, data: { status: TenantUserStatus.ACTIVE } });
});
it("is idempotent for an already active member", async () => {
dbMock.tenantUser.findFirst.mockResolvedValue({ id: "m1", status: TenantUserStatus.ACTIVE });
expect(await reactivateTenantUser({ tenantId: "t1", targetUserId: "u1" })).toEqual({ ok: false, reason: "already-active" });
});
});