import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createPostgresDb } from "@/infra/db/postgres"; import { migrate, pendingMigrations } from "@/infra/db/migrate"; import type { Db } from "@/infra/db/types"; /** * RLS e isolamento multi-tenant contra PostgreSQL real. A suíte padrão usa * PGlite, que é o mesmo motor mas não o mesmo servidor: papel `app_user`, * `force row level security` e `set local role` precisam ser confirmados aqui. */ const url = process.env.ADAPTER_TEST_DATABASE_URL; const enabled = Boolean(url); const ALPHA = "11111111-1111-4111-8111-111111111111"; const BETA = "22222222-2222-4222-8222-222222222222"; const ANA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; const DIEGO = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"; let db: Db; describe.skipIf(!enabled)("PostgreSQL real", () => { beforeAll(async () => { process.env.ADAPTER_APPLY_IDENTITY_SHIM = "1"; db = createPostgresDb(url as string); await db.exec(`drop schema if exists public cascade; create schema public;`); await migrate(db, { applyIdentityShim: true }); await db.withService(async (query) => { await query.query( `insert into public.profiles (id, display_name, is_superadmin) values ($1,'Ana',false), ($2,'Diego',false) on conflict (id) do nothing`, [ANA, DIEGO], ); await query.query( `insert into public.workspaces (id, name) values ($1,'Alpha'), ($2,'Beta') on conflict (id) do nothing`, [ALPHA, BETA], ); await query.query( `insert into public.workspace_members (workspace_id, user_id, role) values ($1,$2,'owner'), ($3,$4,'admin') on conflict (workspace_id, user_id) do nothing`, [ALPHA, ANA, BETA, DIEGO], ); await query.query( `insert into public.workspace_module_flags (workspace_id, module_key, enabled) values ($1,'spreadsheet_adapter',true), ($2,'spreadsheet_adapter',true) on conflict (workspace_id, module_key) do update set enabled = excluded.enabled`, [ALPHA, BETA], ); }); }); afterAll(async () => { if (db) await db.close(); }); it("aplica todas as migrations e não deixa pendência", async () => { expect(await pendingMigrations(db, { applyIdentityShim: true })).toEqual([]); }); it("cria o papel app_user sem bypass de RLS", async () => { const rows = await db.withService((query) => query.query<{ rolbypassrls: boolean; rolsuper: boolean }>( `select rolbypassrls, rolsuper from pg_roles where rolname = 'app_user'`, ), ); expect(rows[0]).toBeDefined(); expect(rows[0].rolbypassrls).toBe(false); expect(rows[0].rolsuper).toBe(false); }); it("força RLS em todas as tabelas do módulo", async () => { const rows = await db.withService((query) => query.query<{ relname: string; relrowsecurity: boolean; relforcerowsecurity: boolean }>( `select relname, relrowsecurity, relforcerowsecurity from pg_class where relname like 'spreadsheet%' or relname in ('profiles','workspaces','workspace_members','audit_log')`, ), ); expect(rows.length).toBeGreaterThan(0); for (const row of rows) { expect(row.relrowsecurity, `${row.relname} sem RLS`).toBe(true); expect(row.relforcerowsecurity, `${row.relname} sem FORCE RLS`).toBe(true); } }); it("não deixa um usuário ver o workspace do outro", async () => { const anaSees = await db.withUser(ANA, (query) => query.query<{ id: string }>(`select id from public.workspaces`), ); expect(anaSees.map((row) => row.id.toLowerCase())).toEqual([ALPHA]); const diegoSees = await db.withUser(DIEGO, (query) => query.query<{ id: string }>(`select id from public.workspaces`), ); expect(diegoSees.map((row) => row.id.toLowerCase())).toEqual([BETA]); }); it("não deixa um usuário ler modelo de outro workspace", async () => { const modelId = "99999999-9999-4999-8999-999999999999"; await db.withService((query) => query.query( `insert into public.spreadsheet_models (id, workspace_id, name, original_filename, format, storage_key, content_sha256, sheet_name, header_row, columns_schema, created_by, status) values ($1,$2,'Modelo Beta','b.xlsx','xlsx',$3,'sha','Plan1',1,'[]'::jsonb,$4,'active') on conflict (id) do nothing`, [modelId, BETA, `workspaces/${BETA}/models/${modelId}/original`, DIEGO], ), ); const anaSees = await db.withUser(ANA, (query) => query.query(`select id from public.spreadsheet_models where id = $1`, [modelId]), ); expect(anaSees).toHaveLength(0); }); it("não deixa o papel da aplicação alterar auditoria", async () => { await db.withService((query) => query.query( `insert into public.audit_log (workspace_id, actor_id, action, resource_type, resource_id, result) values ($1,$2,'test.event','workspace',$1,'success')`, [ALPHA, ANA], ), ); await expect( db.withUser(ANA, (query) => query.query(`update public.audit_log set result = 'tampered'`)), ).rejects.toThrow(); }); it("pula o shim quando a decisão é não aplicar", async () => { const pending = await pendingMigrations(db, { applyIdentityShim: false }); expect(pending).not.toContain("0001_platform_shim.sql"); }); }); describe.skipIf(enabled)("PostgreSQL real", () => { it("PENDENTE: defina ADAPTER_TEST_DATABASE_URL para executar", () => { expect(enabled).toBe(false); }); });