test(int): C149 — isola o advisory lock do harness e2e (vitest fora do processo Playwright)

This commit is contained in:
Francisco Solla 2026-09-12 03:14:31 -03:00
parent 534d5f1d02
commit 8408a15b6d
8 changed files with 62 additions and 52 deletions

View file

@ -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.

View file

@ -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 <T>(key: number, fn: () => Promise<T>): 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<void>> => {
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
})
}
}

View file

@ -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<void>) | null = null
beforeAll(async () => {
release = await acquireAdvisoryLock(GOOGLE_CALENDAR_SYNC_LOCK_KEY)
}, 60_000)
afterAll(async () => {
if (!release) return
await release()
release = null
})
}

View file

@ -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({

View file

@ -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

View file

@ -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({

View file

@ -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({

View file

@ -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({