soul v4.2.0

@jun/soul/testing

Testing

Helpers para montar datos y una bateria de chequeos de seguridad.

Que trae#

Lo necesario para probar un proyecto sobre soul con @cloudflare/vitest-pool-workers: aplicar migraciones, crear usuarios y organizaciones, falsificar cookies de sesion y verificar el aislamiento entre inquilinos.

Migraciones y limpieza#

applySql(db, sql)
Aplica un archivo SQL partiendo las sentencias. Necesario porque db.exec() no tolera SQL multilinea ni comentarios.
SOUL_TABLES
Las tablas del core, en orden seguro de borrado.
resetSoulTables(db, extraTables?)
Vacia el core entre tests. Las tablas hijas van en extraTables y se borran primero.
TypeScript
import { applySql, resetSoulTables } from "@jun/soul/testing";
import coreSql from "@jun/soul/migrations/0001_soul_core.sql?raw";
import misTablas from "../migrations/0002_negocio.sql?raw";

beforeAll(async () => {
  await applySql(env.DB, coreSql);
  await applySql(env.DB, misTablas);
});

beforeEach(async () => {
  await resetSoulTables(env.DB, ["facturas"]);   // hijas primero
});

Datos de prueba#

createTestUser(db, { email, name?, isAdmin? })
Crea un usuario y devuelve su id. No crea organizacion.
createTestOrg(db, { name?, kind?, plan? })
Crea una organizacion.
addTestMember(db, orgId, userId, role?)
Agrega la membresia.
sessionCookieFor(secret, userId, email, cookieName?)
Fabrica una cookie de sesion valida, para saltarse el OAuth.

Chequeos de seguridad#

runSecurityChecks corre de una vez los cuatro errores que mas se repiten:

ChequeoQue espera
Anonimo en ruta protegida302 al login
Cookie falsificada302 al login (la firma no valida)
Usuario normal en ruta de admin403
Recurso de otra organizacion404 (si defines createResource)
TypeScript
import { runSecurityChecks } from "@jun/soul/testing";

it("las defensas basicas estan puestas", async () => {
  const checks = await runSecurityChecks({
    fetch: SELF.fetch.bind(SELF),
    db: env.DB,
    sessionSecret: env.SESSION_SECRET,
    protectedPath: "/facturas",
    adminPaths: ["/admin/users", "/admin/orgs"],
    createResource: async (db, orgId) => {
      const r = await db.prepare(
        "INSERT INTO facturas (org_id, title) VALUES (?, 'x') RETURNING id",
      ).bind(orgId).first<{ id: number }>();
      return `/facturas/${r!.id}`;
    },
  });

  for (const c of checks) expect(c.ok, `${c.name}: ${c.detail}`).toBe(true);
});