79 lines
4.3 KiB
TypeScript
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" });
|
|
});
|
|
});
|