import { test } from "node:test"; import assert from "node:assert/strict"; import { pool } from "../db/pool.ts"; import { resetDb, crearNegocio } from "./helpers.ts"; import { ctxDe, guardarCredencial, olvidarCredencial } from "../crm/ctx.ts"; process.env.CRM_MASTER_KEY = Buffer.alloc(32, 3).toString("base64"); test("dos negocios tienen credenciales distintas y no se cruzan", async () => { await resetDb(); const a = await crearNegocio({ name: "Spa A", slug: "spa-a" }); const b = await crearNegocio({ name: "Spa B", slug: "spa-b" }); await guardarCredencial(a.id, "loc-AAA", "token-de-A", "Spa A"); await guardarCredencial(b.id, "loc-BBB", "token-de-B", "Spa B"); const ctxA = await ctxDe(a.id); const ctxB = await ctxDe(b.id); assert.equal(ctxA.locationId, "loc-AAA"); assert.equal(ctxA.token, "token-de-A"); assert.equal(ctxB.locationId, "loc-BBB"); assert.equal(ctxB.token, "token-de-B"); assert.equal(ctxA.businessId, a.id); }); test("el token no queda en claro en la base", async () => { await resetDb(); const a = await crearNegocio(); await guardarCredencial(a.id, "loc-1", "token-secretisimo"); const { rows } = await pool.query<{ token_cipher: Buffer; token_fingerprint: string }>( `SELECT token_cipher, token_fingerprint FROM crm_connections WHERE business_id = $1`, [a.id] ); assert.ok( !rows[0].token_cipher.toString("utf8").includes("token-secretisimo"), "el cifrado no puede contener el token legible" ); assert.equal(rows[0].token_fingerprint, "etisimo".slice(-6)); }); test("volver a guardar rota la credencial sin duplicar la fila", async () => { await resetDb(); const a = await crearNegocio(); await guardarCredencial(a.id, "loc-1", "token-viejo"); await guardarCredencial(a.id, "loc-1", "token-nuevo"); assert.equal((await ctxDe(a.id)).token, "token-nuevo"); const { rows } = await pool.query<{ n: number }>( `SELECT count(*)::int AS n FROM crm_connections WHERE business_id = $1`, [a.id] ); assert.equal(rows[0].n, 1); }); test("rotar la credencial conserva la etiqueta anterior si no se manda otra", async () => { await resetDb(); const a = await crearNegocio(); await guardarCredencial(a.id, "loc-1", "t1", "Yola Franco Spa"); await guardarCredencial(a.id, "loc-1", "t2"); const { rows } = await pool.query<{ label: string }>( `SELECT label FROM crm_connections WHERE business_id = $1`, [a.id] ); assert.equal(rows[0].label, "Yola Franco Spa"); }); test("un negocio sin conexión da un 409 que dice qué hacer", async () => { await resetDb(); const a = await crearNegocio(); await assert.rejects( () => ctxDe(a.id), (e: any) => { assert.equal(e.status, 409); assert.match(e.error, /no está vinculado/i); return true; } ); }); test("una conexión sin token da un 409 distinto del de sin conexión", async () => { await resetDb(); const a = await crearNegocio(); await pool.query( `INSERT INTO crm_connections (business_id, location_id) VALUES ($1, 'loc-1')`, [a.id] ); await assert.rejects( () => ctxDe(a.id), (e: any) => { assert.equal(e.status, 409); assert.match(e.error, /token/i); return true; } ); }); test("olvidarCredencial borra el token pero conserva la conexión y lo sincronizado", async () => { await resetDb(); const a = await crearNegocio(); await guardarCredencial(a.id, "loc-1", "token-x", "Etiqueta"); await olvidarCredencial(a.id); const { rows } = await pool.query( `SELECT location_id, label, token_cipher, token_fingerprint FROM crm_connections WHERE business_id = $1`, [a.id] ); assert.equal(rows[0].location_id, "loc-1", "la subcuenta se recuerda"); assert.equal(rows[0].label, "Etiqueta"); assert.equal(rows[0].token_cipher, null); assert.equal(rows[0].token_fingerprint, null); // `ctxDe` lanza `{ status, error }`, no un Error: la forma con expresión // regular compara contra `message`, que un objeto plano no tiene. await assert.rejects( () => ctxDe(a.id), (e: any) => { assert.match(e.error, /token/i); return true; } ); });