Files
adaptador-planilhas/tests/services/project-identity.test.ts
T
Ferraz Fagundes b410fa4559
ci / lint, typecheck, testes e build (push) Canceled after 0s
ci / Postgres e Redis reais (push) Canceled after 0s
ci / imagem Docker Linux (push) Canceled after 0s
fix(db): permite inicializacao da identidade sem roles operacionais
2026-10-20 00:25:22 -03:00

414 lines
18 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createPostgresDb } from "@/infra/db/postgres";
import { installProjectIdentity, projectIdentityDefinitionSql, revertProjectIdentity } from "@/infra/db/project-identity";
import { migrate } from "@/infra/db/migrate";
import { assertAdminTarget, assertDatabaseGuard, readProjectIdentity } from "@/infra/db/target-guard";
import type { Db } from "@/infra/db/types";
/**
* Identidade persistente contra PostgreSQL 17 descartável.
* A suíte só roda com ADAPTER_TEST_DATABASE_URL. Sem essa URL o caso fica
* pendente e não conta como aprovação.
*/
const url = process.env.ADAPTER_TEST_DATABASE_URL;
const enabled = Boolean(url);
const REF = "abcd1234abcd1234wxyz";
const OUTRO_REF = "zzzzzzzzzzzzzzzzzzz1";
const env = {
ADAPTER_ENV: "staging",
ADAPTER_DB_PROJECT_REF: REF,
ADAPTER_DB_POOLER_HOST: "aws-1-us-west-2.pooler.supabase.com",
};
let db: Db;
function roleUrl(role: string): string {
const parsed = new URL(url as string);
parsed.username = role;
parsed.password = "";
return parsed.toString();
}
describe.skipIf(!enabled)("identidade no PostgreSQL 17", () => {
beforeAll(async () => {
db = createPostgresDb(url as string);
await db.exec(`
drop schema if exists adaptador_guard cascade;
drop schema if exists public cascade;
create schema public;
drop schema if exists auth cascade;
create schema auth;
drop schema if exists storage cascade;
create schema storage;
`);
await db.exec(`
do $$
begin
if exists (select 1 from pg_catalog.pg_roles where rolname = 'adaptador_web') then
drop role adaptador_web;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'adaptador_worker') then
drop role adaptador_worker;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'app_user') then
drop role app_user;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'ident_owner') then
drop role ident_owner;
end if;
end
$$;
`);
await db.exec(`alter database postgres set "app.settings.jwt_exp" = '3600'`);
});
afterAll(async () => {
if (!db) return;
await db.exec(`drop schema if exists adaptador_guard cascade`).catch(() => undefined);
await db.close();
});
it("grava a primeira identidade sem papéis web ou worker", async () => {
await db.exec(`
drop role if exists anon;
drop role if exists authenticated;
drop role if exists service_role;
create role anon nologin nosuperuser nobypassrls;
create role authenticated nologin nosuperuser nobypassrls;
create role service_role nologin nosuperuser bypassrls;
`);
try {
expect(await installProjectIdentity(db, REF)).toBe("criada");
expect(await installProjectIdentity(db, REF)).toBe("inalterada");
await expect(installProjectIdentity(db, OUTRO_REF)).rejects.toMatchObject({ code: "marcador_diverge" });
const estado = await db.withService((query) =>
query.query<{
operacional: number;
migrations: string | null;
flags: string | null;
anon_exec: boolean;
service_exec: boolean;
project_ref: string;
}>(
`select
(select count(*)::int from pg_catalog.pg_roles where rolname in ('adaptador_web', 'adaptador_worker')) as operacional,
to_regclass('public.schema_migrations')::text as migrations,
to_regclass('public.workspace_module_flags')::text as flags,
has_function_privilege('anon', 'adaptador_guard.adapter_project_identity()', 'execute') as anon_exec,
has_function_privilege('service_role', 'adaptador_guard.adapter_project_identity()', 'execute') as service_exec,
i.project_ref
from adaptador_guard.project_identity i`,
),
);
expect(estado[0]).toEqual({
operacional: 0,
migrations: null,
flags: null,
anon_exec: false,
service_exec: false,
project_ref: REF,
});
} finally {
await revertProjectIdentity(db).catch(async () => {
await db.exec(`drop schema if exists adaptador_guard cascade`).catch(() => undefined);
});
await db.exec(`
drop role if exists anon;
drop role if exists authenticated;
drop role if exists service_role;
`).catch(() => undefined);
}
});
it("não cria a identidade na migration e impede a web de alterá-la", async () => {
await db.exec(`create schema adaptador_guard`);
await expect(installProjectIdentity(db, REF)).rejects.toMatchObject({ code: "marcador_indisponivel" });
const vazio = await db.withService((query) =>
query.query<{ relname: string | null }>(
`select c.relname
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'adaptador_guard' and c.relkind = 'r'`,
),
);
expect(vazio).toEqual([]);
await db.exec(`drop schema adaptador_guard restrict`);
const original = projectIdentityDefinitionSql(REF);
const quebrado = original.replace(
/end\s*\$identity\$;\s*$/,
"raise exception 'falha_injetada';\nend\n$identity$;\n",
);
expect(quebrado).toContain("falha_injetada");
await expect(db.exec(quebrado)).rejects.toThrow(/falha_injetada/);
const aposFalha = await db.withService((query) =>
query.query<{ exists: boolean }>(
`select exists(select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard') as exists`,
),
);
expect(aposFalha[0]?.exists).toBe(false);
await migrate(db, { applyIdentityShim: true });
const antes = await db.withService((query) =>
query.query<{ exists: boolean }>(
`select exists(select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard') as exists`,
),
);
expect(antes[0]?.exists).toBe(false);
await db.exec(`
drop owned by adaptador_web, adaptador_worker, app_user;
drop role adaptador_web;
drop role adaptador_worker;
drop role app_user;
drop schema public cascade;
create schema public;
`);
expect(await installProjectIdentity(db, REF)).toBe("criada");
await migrate(db, { applyIdentityShim: true });
const loginAntes = await db.withService((query) =>
query.query<{ rolname: string; rolcanlogin: boolean; rolsuper: boolean; rolbypassrls: boolean }>(
`select rolname, rolcanlogin, rolsuper, rolbypassrls
from pg_catalog.pg_roles
where rolname in ('adaptador_web', 'adaptador_worker', 'app_user')`,
),
);
expect(loginAntes).toHaveLength(3);
for (const role of loginAntes) {
expect(role.rolcanlogin, role.rolname).toBe(false);
expect(role.rolsuper, role.rolname).toBe(false);
expect(role.rolbypassrls, role.rolname).toBe(false);
}
const semGrant = await db.withService((query) =>
query.query<{ web: boolean; worker: boolean; app: boolean }>(
`select
has_function_privilege('adaptador_web', 'adaptador_guard.adapter_project_identity()', 'execute') as web,
has_function_privilege('adaptador_worker', 'adaptador_guard.adapter_project_identity()', 'execute') as worker,
has_function_privilege('app_user', 'adaptador_guard.adapter_project_identity()', 'execute') as app`,
),
);
expect(semGrant[0]).toEqual({ web: false, worker: false, app: false });
await db.exec(`alter role adaptador_web login; alter role adaptador_worker login`);
const webAntes = createPostgresDb(roleUrl("adaptador_web"));
await expect(assertDatabaseGuard(webAntes, "web", roleUrl("adaptador_web"), env)).rejects.toMatchObject({
code: "marcador_indisponivel",
});
await webAntes.close();
expect(await installProjectIdentity(db, REF)).toBe("inalterada");
const antesDaSegunda = await db.withService((query) =>
query.query<{ project_ref: string; xmin: string }>(
`select project_ref, xmin::text as xmin from adaptador_guard.project_identity`,
),
);
expect(await installProjectIdentity(db, REF)).toBe("inalterada");
const depoisDaSegunda = await db.withService((query) =>
query.query<{ project_ref: string; xmin: string }>(
`select project_ref, xmin::text as xmin from adaptador_guard.project_identity`,
),
);
expect(depoisDaSegunda).toEqual(antesDaSegunda);
await expect(installProjectIdentity(db, OUTRO_REF)).rejects.toMatchObject({ code: "marcador_diverge" });
const aposRefErrado = await db.withService((query) =>
query.query<{ project_ref: string }>(`select project_ref from adaptador_guard.project_identity`),
);
expect(aposRefErrado[0]?.project_ref).toBe(REF);
await assertAdminTarget(db, env);
await assertDatabaseGuard(db, "admin", url, env);
await expect(assertDatabaseGuard(db, "admin", url, { ...env, ADAPTER_DB_PROJECT_REF: OUTRO_REF })).rejects.toMatchObject({
code: "marcador_diverge",
});
const web = createPostgresDb(roleUrl("adaptador_web"));
const worker = createPostgresDb(roleUrl("adaptador_worker"));
try {
await assertDatabaseGuard(web, "web", roleUrl("adaptador_web"), env);
await assertDatabaseGuard(worker, "worker", roleUrl("adaptador_worker"), env);
await expect(
web.exec(`update adaptador_guard.project_identity set project_ref = '${OUTRO_REF}'`),
).rejects.toMatchObject({ code: "42501" });
await expect(worker.exec(`delete from adaptador_guard.project_identity`)).rejects.toMatchObject({ code: "42501" });
} finally {
await web.close();
await worker.close();
}
await expect(db.exec(`update adaptador_guard.project_identity set project_name = project_name`)).rejects.toMatchObject({
code: "42501",
});
await expect(db.exec(`delete from adaptador_guard.project_identity`)).rejects.toMatchObject({ code: "42501" });
await expect(
db.exec(`do $$ begin set local role app_user; perform * from adaptador_guard.adapter_project_identity(); end $$;`),
).rejects.toMatchObject({ code: "42501" });
await db.exec(`
create role ident_owner nologin nosuperuser bypassrls;
grant usage on schema adaptador_guard to ident_owner;
alter table adaptador_guard.project_identity owner to ident_owner;
alter function adaptador_guard.adapter_project_identity() owner to ident_owner;
`);
const comoDono = await db.withService((query) =>
query.query<{ project_name: string; project_ref: string; rolsuper: boolean; rolbypassrls: boolean }>(
`select i.project_name, i.project_ref, r.rolsuper, r.rolbypassrls
from pg_catalog.pg_roles r
cross join adaptador_guard.adapter_project_identity() i
where r.rolname = 'ident_owner'`,
),
);
expect(comoDono[0]).toMatchObject({
project_name: "adaptador-planilha-hml",
project_ref: REF,
rolsuper: false,
rolbypassrls: true,
});
const lida = await readProjectIdentity(db);
expect(lida).toEqual({ projectName: "adaptador-planilha-hml", projectRef: REF });
const marcadores = await db.withService((query) =>
query.query<{ item: string }>(
`select item
from pg_catalog.pg_db_role_setting s
cross join lateral unnest(s.setconfig) as item
where s.setdatabase = (select oid from pg_catalog.pg_database where datname = current_database())
and s.setrole = 0`,
),
);
const itens = marcadores.map((row) => row.item);
expect(itens).toContain("app.settings.jwt_exp=3600");
expect(itens.some((item) => item.startsWith("app.hml_project"))).toBe(false);
const internos = await db.withService((query) =>
query.query<{ schema_name: string; total: number }>(
`select n.nspname as schema_name, count(c.oid)::int as total
from pg_catalog.pg_namespace n
left join pg_catalog.pg_class c on c.relnamespace = n.oid and c.relkind in ('r', 'p', 'v', 'm')
where n.nspname in ('auth', 'storage')
group by n.nspname
order by n.nspname`,
),
);
expect(internos).toEqual([
{ schema_name: "auth", total: 0 },
{ schema_name: "storage", total: 0 },
]);
const proteção = await db.withService((query) =>
query.query<{ relrowsecurity: boolean; relforcerowsecurity: boolean }>(
`select c.relrowsecurity, c.relforcerowsecurity
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'adaptador_guard' and c.relname = 'project_identity'`,
),
);
expect(proteção[0]).toEqual({ relrowsecurity: true, relforcerowsecurity: true });
const definidor = await db.withService((query) =>
query.query<{ proname: string; prosecdef: boolean; proconfig: string[] | null }>(
`select p.proname, p.prosecdef, p.proconfig
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'adaptador_guard'
order by p.proname`,
),
);
expect(definidor).toEqual([
{ proname: "adapter_project_identity", prosecdef: true, proconfig: ["search_path=pg_catalog"] },
{ proname: "reject_identity_change", prosecdef: false, proconfig: ["search_path=pg_catalog"] },
]);
await db.exec(`
create table adaptador_guard.extra (id int);
`);
await expect(installProjectIdentity(db, REF)).rejects.toMatchObject({ code: "marcador_indisponivel" });
await db.exec(`drop table adaptador_guard.extra`);
await db.exec(`
do $$
begin
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'anon') then
create role anon nologin nosuperuser nobypassrls;
end if;
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'authenticated') then
create role authenticated nologin nosuperuser nobypassrls;
end if;
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'service_role') then
create role service_role nologin nosuperuser bypassrls;
end if;
end
$$;
`);
expect(await installProjectIdentity(db, REF)).toBe("inalterada");
const privilegios = await db.withService((query) =>
query.query<{ papel: string; uso: boolean; leitura: boolean; alteracao: boolean; executar: boolean; criar: boolean }>(
`select papel, uso, leitura, alteracao, executar, criar
from (
select 'public'::text as papel,
has_schema_privilege('public', 'adaptador_guard', 'usage') as uso,
has_schema_privilege('public', 'adaptador_guard', 'create') as criar,
has_table_privilege('public', 'adaptador_guard.project_identity', 'select') as leitura,
has_table_privilege('public', 'adaptador_guard.project_identity', 'update') as alteracao,
has_function_privilege('public', 'adaptador_guard.adapter_project_identity()', 'execute') as executar
union all
select r.rolname,
has_schema_privilege(r.oid, 'adaptador_guard', 'usage'),
has_schema_privilege(r.oid, 'adaptador_guard', 'create'),
has_table_privilege(r.oid, 'adaptador_guard.project_identity', 'select'),
has_table_privilege(r.oid, 'adaptador_guard.project_identity', 'update'),
has_function_privilege(r.oid, 'adaptador_guard.adapter_project_identity()', 'execute')
from pg_catalog.pg_roles r
where r.rolname in ('anon', 'authenticated', 'service_role', 'app_user', 'adaptador_web', 'adaptador_worker')
) privilegio
order by papel`,
),
);
const porPapel = new Map(privilegios.map((row) => [row.papel, row]));
for (const papel of ["public", "anon", "authenticated", "service_role", "app_user"]) {
expect(porPapel.get(papel), papel).toMatchObject({ uso: false, leitura: false, alteracao: false, executar: false, criar: false });
}
for (const papel of ["adaptador_web", "adaptador_worker"]) {
expect(porPapel.get(papel), papel).toMatchObject({ uso: true, leitura: false, alteracao: false, executar: true, criar: false });
}
await expect(
db.exec(`do $$ begin set local role service_role; perform * from adaptador_guard.adapter_project_identity(); end $$;`),
).rejects.toMatchObject({ code: "42501" });
await expect(
db.exec(`do $$ begin set local role adaptador_web; create table adaptador_guard.extra (id int); end $$;`),
).rejects.toMatchObject({ code: "42501" });
await db.exec(`create view public.identity_leak as select project_name from adaptador_guard.project_identity`);
await expect(revertProjectIdentity(db)).rejects.toMatchObject({ code: "reversao_recusada" });
const vazou = await db.withService((query) =>
query.query<{ nome: string | null }>(`select to_regclass('public.identity_leak')::text as nome`),
);
expect(vazou[0]?.nome).toBe("identity_leak");
const aindaLa = await db.withService((query) =>
query.query<{ exists: boolean }>(
`select exists(select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard') as exists`,
),
);
expect(aindaLa[0]?.exists).toBe(true);
await db.exec(`drop view public.identity_leak`);
await revertProjectIdentity(db);
const restante = await db.withService((query) =>
query.query<{ nspname: string; profiles: string | null }>(
`select n.nspname, to_regclass('public.profiles')::text as profiles
from pg_catalog.pg_namespace n
where n.nspname in ('public', 'auth', 'storage', 'adaptador_guard')
order by n.nspname`,
),
);
expect(restante.map((row) => row.nspname)).toEqual(["auth", "public", "storage"]);
expect(restante.every((row) => row.profiles === "profiles")).toBe(true);
});
});
describe.skipIf(enabled)("identidade no PostgreSQL 17", () => {
it("PENDENTE: defina ADAPTER_TEST_DATABASE_URL para executar", () => {
expect(enabled).toBe(false);
});
});