103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||
|
|
import { PrismaClient } from "@/generated/prisma/client";
|
||
|
|
import { createHold } from "@/lib/booking";
|
||
|
|
import { resetEmailTransport, sendBookingConfirmation } from "@/lib/email";
|
||
|
|
import { seed, type SeedResult } from "@/lib/seed";
|
||
|
|
|
||
|
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||
|
|
const db = new PrismaClient({ adapter });
|
||
|
|
|
||
|
|
const MAILPIT = "http://localhost:8025";
|
||
|
|
|
||
|
|
let fx: SeedResult;
|
||
|
|
let serviceId: string;
|
||
|
|
let therapistA: string;
|
||
|
|
let roomA: string;
|
||
|
|
let customer: { id: string; email: string };
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
fx = await seed(db);
|
||
|
|
serviceId = fx.services.find((s) => s.name === "60-minute Swedish")!.id;
|
||
|
|
therapistA = fx.therapists[0].id;
|
||
|
|
roomA = fx.rooms[0].id;
|
||
|
|
customer = fx.customers[0];
|
||
|
|
// Point email transport at Mailpit even though .env.test doesn't define it.
|
||
|
|
process.env.SMTP_HOST = "localhost";
|
||
|
|
process.env.SMTP_PORT = "1025";
|
||
|
|
process.env.SMTP_FROM = "TouchBase <noreply@touchbase.local>";
|
||
|
|
resetEmailTransport();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await db.$disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
await db.notification.deleteMany();
|
||
|
|
await db.booking.deleteMany();
|
||
|
|
// Clear Mailpit between tests to keep assertions clean.
|
||
|
|
await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
|
||
|
|
});
|
||
|
|
|
||
|
|
const D = (iso: string) => new Date(iso);
|
||
|
|
|
||
|
|
describe("sendBookingConfirmation", () => {
|
||
|
|
test("delivers an email to Mailpit and writes a sent Notification row", async () => {
|
||
|
|
const hold = await createHold(db, {
|
||
|
|
customerId: customer.id,
|
||
|
|
serviceId,
|
||
|
|
therapistId: therapistA,
|
||
|
|
roomId: roomA,
|
||
|
|
startsAt: D("2026-05-05T14:00:00Z"),
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await sendBookingConfirmation({ db, bookingId: hold.id });
|
||
|
|
expect(result.status).toBe("sent");
|
||
|
|
|
||
|
|
// Notification row recorded
|
||
|
|
const noti = await db.notification.findUnique({
|
||
|
|
where: { id: result.notificationId },
|
||
|
|
});
|
||
|
|
expect(noti?.status).toBe("sent");
|
||
|
|
expect(noti?.template).toBe("booking_confirmation");
|
||
|
|
expect(noti?.bookingId).toBe(hold.id);
|
||
|
|
expect(noti?.userId).toBe(customer.id);
|
||
|
|
expect(noti?.to).toBe(customer.email);
|
||
|
|
expect(noti?.sentAt).toBeTruthy();
|
||
|
|
|
||
|
|
// Mailpit received the message
|
||
|
|
const res = await fetch(`${MAILPIT}/api/v1/messages`);
|
||
|
|
const json = (await res.json()) as { messages: { To: { Address: string }[]; Subject: string }[] };
|
||
|
|
expect(json.messages).toHaveLength(1);
|
||
|
|
expect(json.messages[0].To[0].Address).toBe(customer.email);
|
||
|
|
expect(json.messages[0].Subject).toContain("60-minute Swedish");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("formats local time in the practice timezone (America/Detroit)", async () => {
|
||
|
|
process.env.APP_TZ = "America/Detroit";
|
||
|
|
const hold = await createHold(db, {
|
||
|
|
customerId: customer.id,
|
||
|
|
serviceId,
|
||
|
|
therapistId: therapistA,
|
||
|
|
roomId: roomA,
|
||
|
|
startsAt: D("2026-05-05T14:00:00Z"), // 10:00 EDT
|
||
|
|
});
|
||
|
|
await sendBookingConfirmation({ db, bookingId: hold.id });
|
||
|
|
|
||
|
|
const res = await fetch(`${MAILPIT}/api/v1/messages`);
|
||
|
|
const list = (await res.json()) as { messages: { ID: string }[] };
|
||
|
|
const msgRes = await fetch(`${MAILPIT}/api/v1/message/${list.messages[0].ID}`);
|
||
|
|
const msg = (await msgRes.json()) as { Text: string };
|
||
|
|
// 14:00 UTC → 10:00 EDT
|
||
|
|
expect(msg.Text).toContain("10:00");
|
||
|
|
expect(msg.Text).toContain("EDT");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("missing booking id throws", async () => {
|
||
|
|
await expect(
|
||
|
|
sendBookingConfirmation({ db, bookingId: "no-such-booking" }),
|
||
|
|
).rejects.toThrow(/not found/);
|
||
|
|
});
|
||
|
|
});
|