diff --git a/scripts/init-project-identity.ts b/scripts/init-project-identity.ts index d91f25a..62f82e3 100644 --- a/scripts/init-project-identity.ts +++ b/scripts/init-project-identity.ts @@ -1,12 +1,15 @@ +import { adapterEnv } from "../src/config/env"; import { buildPostgresOptions, databaseUrlFor, DatabaseGuardError } from "../src/infra/db/connection-policy"; import { createPostgresDb } from "../src/infra/db/postgres"; import { assertInitAuthorized, installProjectIdentity } from "../src/infra/db/project-identity"; import { assertAdminTarget } from "../src/infra/db/target-guard"; -import { bootstrapScript, runScript } from "./_bootstrap"; +import { runScript } from "./_bootstrap"; /** * Grava `adaptador_guard.project_identity` uma única vez. * Exige ADAPTER_INIT_PROJECT_IDENTITY=1 e o destino administrativo já conferido. + * Não chama o fail-fast da aplicação: nesta etapa os papéis web e worker ainda + * não existem, e Redis, seed e a feature flag ficam fora desta operação. * Migrate, seed e deploy não chamam este script. */ try { @@ -17,7 +20,7 @@ try { process.exit(1); } -bootstrapScript("init-project-identity"); +console.log(`[init-project-identity] ambiente=${adapterEnv()}`); runScript("init-project-identity", async () => { const databaseUrl = databaseUrlFor("admin"); diff --git a/tests/services/project-identity.test.ts b/tests/services/project-identity.test.ts index cffbc07..575f875 100644 --- a/tests/services/project-identity.test.ts +++ b/tests/services/project-identity.test.ts @@ -22,7 +22,7 @@ const env = { let db: Db; -function roleUrl(role: "adaptador_web" | "adaptador_worker"): string { +function roleUrl(role: string): string { const parsed = new URL(url as string); parsed.username = role; parsed.password = ""; @@ -68,6 +68,58 @@ describe.skipIf(!enabled)("identidade no PostgreSQL 17", () => { 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" }); diff --git a/tests/unit/database-guard.test.ts b/tests/unit/database-guard.test.ts index f99513e..7185fce 100644 --- a/tests/unit/database-guard.test.ts +++ b/tests/unit/database-guard.test.ts @@ -98,6 +98,10 @@ describe("barreira do banco de homologação", () => { const initScript = readFileSync(path.join(process.cwd(), "scripts/init-project-identity.ts"), "utf8"); expect(initScript).not.toContain("console.log(databaseUrl"); expect(initScript).not.toContain("console.log(process.env"); + expect(initScript).not.toContain("bootstrapScript"); + expect(initScript).not.toContain("exitOnInvalidConfig"); + expect(initScript).not.toMatch(/migrate\(/); + expect(initScript).not.toMatch(/from ["'].*seed/); }); it("exige o host do Dashboard e recusa índice, região ou domínio diferentes", () => {