import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import { pool } from "../db/pool.ts"; import { createApp } from "../index.ts"; import { resetDb, seedMinimal } from "./helpers.ts"; import type { Server } from "node:http"; let ids: Awaited>; let server: Server; let base: string; before(async () => { await resetDb(); ids = await seedMinimal(); server = createApp().listen(0); base = `http://127.0.0.1:${(server.address() as { port: number }).port}`; }); after(async () => { server.close(); await pool.end(); }); function req(path: string, init: RequestInit = {}, userId = ids.ownerUserId) { return fetch(`${base}${path}`, { ...init, headers: { "content-type": "application/json", authorization: `Bearer ${userId}`, ...(init.headers || {}), }, }); } const nueva = (start: string) => ({ client_id: ids.clientId, employee_id: ids.employeeId, service_id: ids.serviceId, start_at: start, }); test("crea una cita y calcula el fin con la duración del servicio", async () => { const r = await req("/api/appointments", { method: "POST", body: JSON.stringify(nueva("2026-09-02T16:00:00Z")), }); assert.equal(r.status, 201); const { appointment } = await r.json(); // El servicio sembrado dura 90 min. assert.equal(appointment.end_at, "2026-09-02T17:30:00Z"); assert.equal(appointment.price, 850); assert.equal(appointment.status, "scheduled"); }); test("un solape devuelve 409 en español, no un 500", async () => { const r = await req("/api/appointments", { method: "POST", body: JSON.stringify(nueva("2026-09-02T17:00:00Z")), }); assert.equal(r.status, 409); const body = await r.json(); assert.match(body.error, /ocupad/i); }); test("reprogramar a un hueco libre funciona", async () => { const { rows } = await pool.query(`SELECT id FROM appointments ORDER BY id LIMIT 1`); const r = await req(`/api/appointments/${rows[0].id}`, { method: "PATCH", body: JSON.stringify({ start_at: "2026-09-02T19:00:00Z" }), }); assert.equal(r.status, 200); const { appointment } = await r.json(); assert.equal(appointment.start_at, "2026-09-02T19:00:00Z"); assert.equal(appointment.end_at, "2026-09-02T20:30:00Z"); }); test("reprogramar encima de otra cita devuelve 409", async () => { await req("/api/appointments", { method: "POST", body: JSON.stringify(nueva("2026-09-02T12:00:00Z")), }); const { rows } = await pool.query( `SELECT id FROM appointments WHERE start_at = '2026-09-02T12:00:00Z'` ); const r = await req(`/api/appointments/${rows[0].id}`, { method: "PATCH", body: JSON.stringify({ start_at: "2026-09-02T19:30:00Z" }), }); assert.equal(r.status, 409); }); test("cancelar libera el hueco y registra quién canceló", async () => { const { rows } = await pool.query( `SELECT id FROM appointments WHERE start_at = '2026-09-02T19:00:00Z'` ); const r = await req(`/api/appointments/${rows[0].id}/cancel`, { method: "POST", body: JSON.stringify({ cancelled_by: "client", reason: "Se enfermó" }), }); assert.equal(r.status, 200); const { appointment } = await r.json(); assert.equal(appointment.status, "cancelled"); assert.equal(appointment.cancelled_by, "client"); const libre = await req("/api/appointments", { method: "POST", body: JSON.stringify(nueva("2026-09-02T19:00:00Z")), }); assert.equal(libre.status, 201, "el hueco de una cancelada vuelve a estar libre"); }); test("cada cambio deja un evento en appointment_events", async () => { const { rows } = await pool.query(`SELECT action FROM appointment_events ORDER BY id`); const acciones = rows.map((r) => r.action); assert.ok(acciones.includes("created")); assert.ok(acciones.includes("rescheduled")); assert.ok(acciones.includes("cancelled")); }); test("no se puede tocar una cita de otro negocio", async () => { const other = await pool.query( `INSERT INTO businesses (name, slug, working_hours) VALUES ('Otro Spa 2','otro-2','{}'::jsonb) RETURNING id` ); const otherUser = await pool.query( `INSERT INTO users (business_id, email, password, name, role) VALUES ($1,'otro2@otro.mx','x','Otra','owner') RETURNING id`, [other.rows[0].id] ); const { rows } = await pool.query(`SELECT id FROM appointments ORDER BY id LIMIT 1`); const r = await req( `/api/appointments/${rows[0].id}`, { method: "PATCH", body: JSON.stringify({ notes: "intruso" }) }, otherUser.rows[0].id ); assert.equal(r.status, 404); });