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 { guardarCredencial } from "../crm/ctx.ts"; import type { Server } from "node:http"; process.env.CRM_MASTER_KEY = Buffer.alloc(32, 5).toString("base64"); let ids: Awaited>; let adminId: number; let server: Server; let base: string; before(async () => { await resetDb(); ids = await seedMinimal(); const { rows } = await pool.query( `INSERT INTO users (business_id, email, password, name, role) VALUES (NULL,'plataforma@agendamax.mx','demo1234','Plataforma','admin') RETURNING id` ); adminId = rows[0].id; 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: number = adminId) { return fetch(`${base}${path}`, { ...init, headers: { "content-type": "application/json", authorization: `Bearer ${userId}`, ...(init.headers || {}), }, }); } test("una dueña de negocio no puede entrar a la consola de plataforma", async () => { const r = await req("/api/admin/businesses", {}, ids.ownerUserId); assert.equal(r.status, 403); }); test("una empleada tampoco", async () => { const r = await req("/api/admin/businesses", {}, ids.employeeUserId); assert.equal(r.status, 403); }); test("el superadministrador da de alta una cuenta con su dueña", async () => { const r = await req("/api/admin/businesses", { method: "POST", body: JSON.stringify({ name: "Spa Nuevo", owner_email: "Duena@SpaNuevo.MX", owner_name: "Dueña", owner_password: "demo1234", }), }); assert.equal(r.status, 201); const b = await r.json(); assert.equal(b.business.name, "Spa Nuevo"); assert.equal(b.business.slug, "spa-nuevo", "el negocio nace con slug"); assert.equal(b.owner.email, "duena@spanuevo.mx", "el correo se normaliza a minúsculas"); // Sin horario, un negocio nuevo no tiene ninguna franja agendable. const { rows } = await pool.query( `SELECT working_hours FROM businesses WHERE id = $1`, [b.business.id] ); assert.ok(rows[0].working_hours?.["1"], "el negocio nace con horario laboral"); }); test("dos cuentas con el mismo nombre no chocan de slug", async () => { const r = await req("/api/admin/businesses", { method: "POST", body: JSON.stringify({ name: "Spa Nuevo", owner_email: "otra@spa.mx", owner_name: "Otra", owner_password: "x", }), }); assert.equal(r.status, 201); assert.equal((await r.json()).business.slug, "spa-nuevo-2"); }); test("un correo repetido se rechaza con 409, no con un 500 de la base", async () => { const r = await req("/api/admin/businesses", { method: "POST", body: JSON.stringify({ name: "Tercero", owner_email: "duena@spanuevo.mx", owner_name: "X", owner_password: "x", }), }); assert.equal(r.status, 409); }); test("faltar datos de la dueña da 400 y lo dice", async () => { const r = await req("/api/admin/businesses", { method: "POST", body: JSON.stringify({ name: "Sin dueña" }), }); assert.equal(r.status, 400); assert.match((await r.json()).error, /dueña/i); }); test("el listado nunca devuelve el token, solo su huella", async () => { await guardarCredencial(ids.businessId, "loc-1", "token-secretisimo", "Yola Franco Spa"); const r = await req("/api/admin/businesses"); assert.equal(r.status, 200); const texto = JSON.stringify(await r.json()); assert.ok(!texto.includes("token-secretisimo"), "el token no puede salir por la API"); assert.ok(texto.includes("etisimo".slice(-6)), "sí debe salir la huella"); assert.ok(texto.includes("Yola Franco Spa"), "y la etiqueta de la subcuenta"); }); test("suspender una cuenta la deja suspendida", async () => { const r = await req(`/api/admin/businesses/${ids.businessId}`, { method: "PATCH", body: JSON.stringify({ status: "suspended" }), }); assert.equal(r.status, 200); assert.equal((await r.json()).business.status, "suspended"); }); test("un estado inventado se rechaza", async () => { const r = await req(`/api/admin/businesses/${ids.businessId}`, { method: "PATCH", body: JSON.stringify({ status: "lo-que-sea" }), }); assert.equal(r.status, 400); }); test("desvincular borra la credencial y conserva la subcuenta", async () => { await guardarCredencial(ids.businessId, "loc-9", "token-x"); const r = await req(`/api/admin/businesses/${ids.businessId}/crm`, { method: "DELETE" }); assert.equal(r.status, 200); const { rows } = await pool.query( `SELECT location_id, token_cipher FROM crm_connections WHERE business_id = $1`, [ids.businessId] ); assert.equal(rows[0].location_id, "loc-9"); assert.equal(rows[0].token_cipher, null); }); test("vincular sin token o sin subcuenta da 400", async () => { const r = await req(`/api/admin/businesses/${ids.businessId}/crm`, { method: "PUT", body: JSON.stringify({ location_id: "loc-1" }), }); assert.equal(r.status, 400); });