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