112 lines
4.6 KiB
TypeScript
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();
|
|
});
|
|
});
|