From 8408a15b6d342ed2aab3c81217bb67e423214775 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 12 Sep 2026 03:14:31 -0300 Subject: [PATCH] =?UTF-8?q?test(int):=20C149=20=E2=80=94=20isola=20o=20adv?= =?UTF-8?q?isory=20lock=20do=20harness=20e2e=20(vitest=20fora=20do=20proce?= =?UTF-8?q?sso=20Playwright)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/c149-conectar-google-oauth-impl.md | 14 ++++---- tests/helpers/advisoryLock.ts | 36 +++++++++---------- tests/helpers/googleCalendarSyncLock.ts | 29 +++++++++++++++ tests/int/googleCalendarLock.int.spec.ts | 7 ++-- tests/int/googleCalendarOAuth.int.spec.ts | 7 ++-- .../int/googleCalendarOAuthAction.int.spec.ts | 7 ++-- tests/int/googleCalendarSync.int.spec.ts | 7 ++-- .../int/googleCalendarSyncAction.int.spec.ts | 7 ++-- 8 files changed, 62 insertions(+), 52 deletions(-) create mode 100644 tests/helpers/googleCalendarSyncLock.ts diff --git a/docs/plans/c149-conectar-google-oauth-impl.md b/docs/plans/c149-conectar-google-oauth-impl.md index 88173468..0a6eca19 100644 --- a/docs/plans/c149-conectar-google-oauth-impl.md +++ b/docs/plans/c149-conectar-google-oauth-impl.md @@ -163,13 +163,13 @@ Estado da conexão é **derivado** (nunca armazenado), função de `oauthRefresh ## Débitos (triage do /simplify) -| ID | Achado | Score | Tipo | Destino | -| --- | ------------------------------------------------------------------- | ----- | ------------- | ------------------------------------------------------------- | -| S1 | Cookie HMAC — 3ª cópia (`googleCalendarOAuth` vs WebAuthn vs import) | 3 | defer_trigger | Defer: extrair `signedCampaignCookie` no 4º cookie assinado ou no próximo toque de um dos três | -| S2 | Refresh token OAuth em texto puro no DB | 3 | defer_trigger | Defer: cifrar em repouso quando compliance/counsel exigir (o campo já é `read: false` e o precedente do repo é texto puro) | -| S3 | Upsert do singleton sem constraint única | 2 | defer_trigger | Defer para **C150** (dono do `calendarId`): decidir constraint se twins virarem risco real | -| S4 | Helper de env dos testes duplicado em 3 specs | 2 | cheap_polish | Descartado (barato, local; os specs estão verdes) | -| S5 | `docToView` com 3 flags posicionais | 1 | cheap_polish | Descartado (cosmético) | +| ID | Achado | Score | Tipo | Destino | +| --- | -------------------------------------------------------------------- | ----- | ------------- | -------------------------------------------------------------------------------------------------------------------------- | +| S1 | Cookie HMAC — 3ª cópia (`googleCalendarOAuth` vs WebAuthn vs import) | 3 | defer_trigger | Defer: extrair `signedCampaignCookie` no 4º cookie assinado ou no próximo toque de um dos três | +| S2 | Refresh token OAuth em texto puro no DB | 3 | defer_trigger | Defer: cifrar em repouso quando compliance/counsel exigir (o campo já é `read: false` e o precedente do repo é texto puro) | +| S3 | Upsert do singleton sem constraint única | 2 | defer_trigger | Defer para **C150** (dono do `calendarId`): decidir constraint se twins virarem risco real | +| S4 | Helper de env dos testes duplicado em 3 specs | 2 | cheap_polish | Descartado (barato, local; os specs estão verdes) | +| S5 | `docToView` com 3 flags posicionais | 1 | cheap_polish | Descartado (cosmético) | - **Já resolvido no simplify (não reabrir):** índice em `oauthConnectedAt` removido; `failure()` sem diagnóstico falso; copy do card ajustada; `Date.parse` na derivação; guarda do `parseAccessToken`; validação de escopo concedido; prefixo morto do manifest; callback grava erro só do exchange; `runAction` no dialog; classify puro `oauthErrorPatchFor`; serialização int. - **Explicitamente fora:** criptografia do refresh token agora; revogação remota no Google (só orientação); rate limiting nas rotas OAuth; verificação completa do app Google; aposentadoria da service account. diff --git a/tests/helpers/advisoryLock.ts b/tests/helpers/advisoryLock.ts index 6c6bba2f..dfd9c3a4 100644 --- a/tests/helpers/advisoryLock.ts +++ b/tests/helpers/advisoryLock.ts @@ -1,5 +1,4 @@ import { Client } from 'pg' -import { afterAll, beforeAll } from 'vitest' import { assertTestDatabase } from './assertTestDatabase' @@ -18,8 +17,11 @@ export const ADMIN_LOGIN_LOCK_KEY = 727_001 * in parallel against one database: several google specs create/pick/delete * configured rows concurrently, and the loader's "configured row wins" * preference then resolves to ANOTHER file's row (channel/count assertions - * read the wrong doc, snapshots land on a row being deleted). The lock - * serializes the whole google calendar family for the duration of each file. + * read the wrong doc, snapshots land on a row being deleted). Serialize the + * whole google calendar family for the duration of each file with + * `serializeGoogleCalendarSyncSpec` (`tests/helpers/googleCalendarSyncLock.ts` + * — the vitest hooks live there so this module stays loadable by Playwright + * fixtures, which must never pull `vitest` into the e2e process). */ export const GOOGLE_CALENDAR_SYNC_LOCK_KEY = 727_002 @@ -46,24 +48,18 @@ export const withAdvisoryLock = async (key: number, fn: () => Promise): Pr } /** - * Holds the given advisory lock for the WHOLE spec file: `beforeAll` acquires - * on a dedicated connection and `afterAll` releases it. Call at the top level - * of a spec. A crashed worker drops its connection, releasing the lock. + * Acquires the lock on a dedicated connection and returns the release + * function. The caller owns the lifetime (a spec file holds it between its + * `beforeAll` and `afterAll`); a crashed worker drops the connection and the + * lock is released by Postgres. */ -export const serializeSpecWithAdvisoryLock = (key: number): void => { - let client: Client | null = null - - beforeAll(async () => { - assertTestDatabase(process.env.DATABASE_URL) - client = new Client({ connectionString: process.env.DATABASE_URL }) - await client.connect() - await client.query('SELECT pg_advisory_lock($1)', [key]) - }, 60_000) - - afterAll(async () => { - if (!client) return +export const acquireAdvisoryLock = async (key: number): Promise<() => Promise> => { + assertTestDatabase(process.env.DATABASE_URL) + const client = new Client({ connectionString: process.env.DATABASE_URL }) + await client.connect() + await client.query('SELECT pg_advisory_lock($1)', [key]) + return async () => { await client.query('SELECT pg_advisory_unlock($1)', [key]).catch(() => {}) await client.end().catch(() => {}) - client = null - }) + } } diff --git a/tests/helpers/googleCalendarSyncLock.ts b/tests/helpers/googleCalendarSyncLock.ts new file mode 100644 index 00000000..f656da78 --- /dev/null +++ b/tests/helpers/googleCalendarSyncLock.ts @@ -0,0 +1,29 @@ +import { afterAll, beforeAll } from 'vitest' + +import { acquireAdvisoryLock, GOOGLE_CALENDAR_SYNC_LOCK_KEY } from './advisoryLock' + +/** + * C149 — holds the google-calendar advisory lock for the WHOLE spec file + * (`beforeAll` acquires on a dedicated connection, `afterAll` releases it). + * Call at the top level of every int spec that touches the singleton + * `googleCalendarSync` row so parallel files cannot read/write each other's + * configured row. + * + * This module imports vitest hooks ON PURPOSE and must only be imported by + * int specs: `tests/helpers/advisoryLock.ts` is also loaded by Playwright + * fixtures, and pulling `vitest` into the e2e process breaks its matchers + * (`Cannot redefine property: Symbol($$jest-matchers-object)`). + */ +export const serializeGoogleCalendarSyncSpec = (): void => { + let release: (() => Promise) | null = null + + beforeAll(async () => { + release = await acquireAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) + }, 60_000) + + afterAll(async () => { + if (!release) return + await release() + release = null + }) +} diff --git a/tests/int/googleCalendarLock.int.spec.ts b/tests/int/googleCalendarLock.int.spec.ts index 96caf2d3..9c4027f0 100644 --- a/tests/int/googleCalendarLock.int.spec.ts +++ b/tests/int/googleCalendarLock.int.spec.ts @@ -7,17 +7,14 @@ import config from '@/payload.config' import { loadGoogleCalendarSyncConfig } from '@/utilities/googleCalendarSync' import { hookFilledCreateData } from '@/utilities/hookFilledData' -import { - GOOGLE_CALENDAR_SYNC_LOCK_KEY, - serializeSpecWithAdvisoryLock, -} from '../helpers/advisoryLock' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock' import { createSlowGoogleFetch, withValidGoogleCalendarCredential, } from '../helpers/googleCalendarTestKey' -serializeSpecWithAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) +serializeGoogleCalendarSyncSpec() let payload: Payload const campaignFixtures = installCampaignFixtures({ diff --git a/tests/int/googleCalendarOAuth.int.spec.ts b/tests/int/googleCalendarOAuth.int.spec.ts index ddeaa05c..bd0e94c3 100644 --- a/tests/int/googleCalendarOAuth.int.spec.ts +++ b/tests/int/googleCalendarOAuth.int.spec.ts @@ -24,12 +24,9 @@ import { recordGoogleCalendarOAuthError, } from '@/utilities/googleCalendarSync' -import { - GOOGLE_CALENDAR_SYNC_LOCK_KEY, - serializeSpecWithAdvisoryLock, -} from '../helpers/advisoryLock' +import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock' -serializeSpecWithAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) +serializeGoogleCalendarSyncSpec() let payload: Payload diff --git a/tests/int/googleCalendarOAuthAction.int.spec.ts b/tests/int/googleCalendarOAuthAction.int.spec.ts index 85d66b51..d50e87ce 100644 --- a/tests/int/googleCalendarOAuthAction.int.spec.ts +++ b/tests/int/googleCalendarOAuthAction.int.spec.ts @@ -52,13 +52,10 @@ import { } from '@/utilities/googleCalendarOAuth' import { recordGoogleCalendarOAuthConnection } from '@/utilities/googleCalendarSync' -import { - GOOGLE_CALENDAR_SYNC_LOCK_KEY, - serializeSpecWithAdvisoryLock, -} from '../helpers/advisoryLock' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock' -serializeSpecWithAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) +serializeGoogleCalendarSyncSpec() let payload: Payload const campaignFixtures = installCampaignFixtures({ diff --git a/tests/int/googleCalendarSync.int.spec.ts b/tests/int/googleCalendarSync.int.spec.ts index ef1b2d40..ffa0ef05 100644 --- a/tests/int/googleCalendarSync.int.spec.ts +++ b/tests/int/googleCalendarSync.int.spec.ts @@ -37,14 +37,11 @@ import { } from '@/utilities/googleCalendarSync' import { hookFilledCreateData } from '@/utilities/hookFilledData' -import { - GOOGLE_CALENDAR_SYNC_LOCK_KEY, - serializeSpecWithAdvisoryLock, -} from '../helpers/advisoryLock' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock' import { withGoogleCalendarTestCredential } from '../helpers/googleCalendarTestKey' -serializeSpecWithAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) +serializeGoogleCalendarSyncSpec() let payload: Payload const campaignFixtures = installCampaignFixtures({ diff --git a/tests/int/googleCalendarSyncAction.int.spec.ts b/tests/int/googleCalendarSyncAction.int.spec.ts index ba5ead61..84acbea1 100644 --- a/tests/int/googleCalendarSyncAction.int.spec.ts +++ b/tests/int/googleCalendarSyncAction.int.spec.ts @@ -10,11 +10,8 @@ import { import config from '@/payload.config' import { loadGoogleCalendarSyncConfig } from '@/utilities/googleCalendarSync' -import { - GOOGLE_CALENDAR_SYNC_LOCK_KEY, - serializeSpecWithAdvisoryLock, -} from '../helpers/advisoryLock' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock' import { withGoogleCalendarTestCredential } from '../helpers/googleCalendarTestKey' /** @@ -34,7 +31,7 @@ vi.mock('@/utilities/campaignActionContext', async (importOriginal) => { import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' -serializeSpecWithAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY) +serializeGoogleCalendarSyncSpec() let payload: Payload const campaignFixtures = installCampaignFixtures({