Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/services/userService.test.ts
T

99 lines
4.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server";
import EventsService from "~/modules/events/services/.server/EventsService";
import { deleteUserWithItsTenants } from "./userService";
import { deleteUser } from "../db/users.db.server";
import { deleteAndCancelTenant } from "./tenantService";
const dbMock = vi.hoisted(() => ({
tenant: { findMany: vi.fn() },
tenantUser: { findMany: vi.fn() },
}));
vi.mock("../db.server", () => ({ db: dbMock }));
vi.mock("../cache.server", () => ({ clearCacheKey: vi.fn() }));
vi.mock("../db/permissions/userRoles.db.server", () => ({ createUserRoles: vi.fn(), deleteAllUserRoles: vi.fn() }));
vi.mock("../db/tenants.db.server", () => ({}));
vi.mock("../db/users.db.server", () => ({ deleteUser: vi.fn() }));
vi.mock("./tenantService", () => ({ deleteAndCancelTenant: vi.fn() }));
vi.mock("~/modules/events/services/.server/EventsService", () => ({
default: { createUserEventBeforeMutation: vi.fn(async (_args, mutation) => mutation()) },
}));
const actorUserId = "user_1";
const targetUser = {
id: actorUserId,
email: "user-1@example.test",
firstName: "Ada",
lastName: "Lovelace",
};
const soloTenant = { id: "tenant_solo", name: "Solo" };
const sharedTenant = { id: "tenant_shared", name: "Shared" };
function deleteInAuthenticatedContext() {
return runWithRlsContext({ userId: actorUserId, tenantId: "tenant_active" }, () =>
deleteUserWithItsTenants({ actorUserId, targetUser, request: new Request("https://app.example.test/settings/profile") })
);
}
describe("deleteUserWithItsTenants", () => {
beforeEach(() => {
vi.clearAllMocks();
dbMock.tenant.findMany.mockImplementation(async () => {
expect(getRlsExecutionContext()).toEqual({ userId: actorUserId });
return [soloTenant, sharedTenant] as never;
});
dbMock.tenantUser.findMany.mockImplementation(async () => {
const tenantId = getRlsExecutionContext()?.tenantId;
return tenantId === soloTenant.id ? [{ userId: actorUserId }] : [{ userId: actorUserId }, { userId: "user_2" }];
});
vi.mocked(deleteAndCancelTenant).mockImplementation(async ({ tenantId, authorizeDeletion }) => {
expect(getRlsExecutionContext()).toEqual({ userId: actorUserId, tenantId });
expect(await authorizeDeletion?.()).toBe(true);
return tenantId === soloTenant.id ? (soloTenant as never) : (null as never);
});
vi.mocked(deleteUser).mockResolvedValue({ id: actorUserId } as never);
});
it("re-proves each tenant boundary and deletes only a sole-owned tenant", async () => {
const result = await deleteInAuthenticatedContext();
expect(dbMock.tenantUser.findMany).toHaveBeenCalledTimes(3);
expect(deleteAndCancelTenant).toHaveBeenCalledTimes(1);
expect(deleteAndCancelTenant).toHaveBeenCalledWith(expect.objectContaining({ tenantId: soloTenant.id, userId: actorUserId }));
expect(deleteUser).toHaveBeenCalledWith(actorUserId);
expect(result.deletedTenants).toEqual([soloTenant]);
expect(EventsService.createUserEventBeforeMutation).toHaveBeenCalledWith(
expect.objectContaining({ event: "user.profile.deleted", userId: actorUserId }),
expect.any(Function)
);
});
it("does not trust a listed tenant when the exact-context membership proof is missing", async () => {
dbMock.tenantUser.findMany.mockResolvedValue([]);
await deleteInAuthenticatedContext();
expect(deleteAndCancelTenant).not.toHaveBeenCalled();
expect(deleteUser).toHaveBeenCalledWith(actorUserId);
});
it("does not delete the user if tenant cancellation fails", async () => {
vi.mocked(deleteAndCancelTenant).mockRejectedValueOnce(new Error("billing unavailable") as never);
await expect(deleteInAuthenticatedContext()).rejects.toThrow("billing unavailable");
expect(deleteUser).not.toHaveBeenCalled();
expect(EventsService.createUserEventBeforeMutation).not.toHaveBeenCalled();
});
it("rejects a caller that tries to isolate a different authenticated actor", async () => {
await expect(
runWithRlsContext({ userId: "attacker", tenantId: "tenant_active" }, () => deleteUserWithItsTenants({ actorUserId, targetUser }))
).rejects.toThrow("matching authenticated user context");
expect(dbMock.tenant.findMany).not.toHaveBeenCalled();
expect(deleteUser).not.toHaveBeenCalled();
});
});