Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/booking/db/booking.db.server.security.test.ts
T

112 lines
4.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
const mocks = vi.hoisted(() => {
const tx = {
bookingPage: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn() },
bookingPageProvider: { deleteMany: vi.fn(), createMany: vi.fn() },
tenantUser: { findMany: vi.fn() },
form: { findFirst: vi.fn() },
};
return {
tx,
withTenantRLS: vi.fn(async (_tenantId: string, callback: (client: typeof tx) => Promise<unknown>) => callback(tx)),
};
});
vi.mock("~/utils/db.server", () => ({ db: {} }));
vi.mock("~/utils/db/.server/tenantRls.server", () => ({ withTenantRLS: mocks.withTenantRLS }));
import { createBookingPage, updateBookingPage, type BookingPageInput } from "./booking.db.server";
const input: BookingPageInput = {
tenantId: "tenant-a",
createdByUserId: "admin-a",
slug: "beratung",
title: "Beratung",
description: null,
durationMinutes: 30,
confirmationEmailSubject: "Bestätigt",
confirmationEmailBody: "Dein Termin ist bestätigt.",
isPublished: true,
providerUserIds: ["provider-a"],
intakeFormId: "form-a",
};
describe("booking page tenant boundaries", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.tx.bookingPage.findFirst.mockResolvedValue({ id: "page-a" });
mocks.tx.bookingPage.create.mockResolvedValue({ id: "page-a" });
mocks.tx.tenantUser.findMany.mockResolvedValue([{ userId: "provider-a" }]);
mocks.tx.form.findFirst.mockResolvedValue({ id: "form-a" });
});
it("rejects a booking page owned by another tenant before any write", async () => {
mocks.tx.bookingPage.findFirst.mockResolvedValueOnce(null);
await expect(updateBookingPage("page-from-tenant-b", input)).resolves.toBe(false);
expect(mocks.withTenantRLS).toHaveBeenCalledWith("tenant-a", expect.any(Function));
expect(mocks.tx.bookingPage.findFirst).toHaveBeenCalledWith({
where: { id: "page-from-tenant-b", tenantId: "tenant-a", archivedAt: null },
select: { id: true },
});
expect(mocks.tx.tenantUser.findMany).not.toHaveBeenCalled();
expect(mocks.tx.bookingPage.update).not.toHaveBeenCalled();
expect(mocks.tx.bookingPageProvider.deleteMany).not.toHaveBeenCalled();
expect(mocks.tx.bookingPageProvider.createMany).not.toHaveBeenCalled();
});
it("rejects a provider who is not an active member of the tenant", async () => {
mocks.tx.tenantUser.findMany.mockResolvedValueOnce([]);
await expect(updateBookingPage("page-a", { ...input, providerUserIds: ["provider-from-tenant-b"] })).resolves.toBe(false);
expect(mocks.tx.tenantUser.findMany).toHaveBeenCalledWith({
where: {
tenantId: "tenant-a",
userId: { in: ["provider-from-tenant-b"] },
status: TenantUserStatus.ACTIVE,
},
select: { userId: true },
});
expect(mocks.tx.bookingPage.update).not.toHaveBeenCalled();
expect(mocks.tx.bookingPageProvider.deleteMany).not.toHaveBeenCalled();
});
it("rejects an intake form that is not visible in the tenant", async () => {
mocks.tx.form.findFirst.mockResolvedValueOnce(null);
await expect(updateBookingPage("page-a", { ...input, intakeFormId: "form-from-tenant-b" })).resolves.toBe(false);
expect(mocks.tx.form.findFirst).toHaveBeenCalledWith({
where: { id: "form-from-tenant-b", tenantId: "tenant-a", status: { not: "archived" } },
select: { id: true },
});
expect(mocks.tx.bookingPage.update).not.toHaveBeenCalled();
expect(mocks.tx.bookingPageProvider.deleteMany).not.toHaveBeenCalled();
});
it("tenant-scopes the page update and provider replacement", async () => {
await expect(updateBookingPage("page-a", { ...input, providerUserIds: ["provider-a", "provider-a"] })).resolves.toBe(true);
expect(mocks.tx.bookingPage.update).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "page-a", tenantId: "tenant-a" } }));
expect(mocks.tx.bookingPageProvider.deleteMany).toHaveBeenCalledWith({
where: { bookingPageId: "page-a", tenantId: "tenant-a" },
});
expect(mocks.tx.bookingPageProvider.createMany).toHaveBeenCalledWith({
data: [{ tenantId: "tenant-a", bookingPageId: "page-a", providerUserId: "provider-a", displayOrder: 0 }],
});
});
it("validates references before creating a booking page", async () => {
mocks.tx.form.findFirst.mockResolvedValueOnce(null);
await expect(createBookingPage({ ...input, intakeFormId: "form-from-tenant-b" })).resolves.toBeNull();
expect(mocks.tx.bookingPage.create).not.toHaveBeenCalled();
expect(mocks.tx.bookingPageProvider.createMany).not.toHaveBeenCalled();
});
});