mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
F1: purgeOrphanedConsentRenames sob lease exclusivo (autocommit) nos 3 pontos que tocam a key canônica; renames de teste migrados para chaves privadas; F1b: purge de contatos órfãos no purgeMunicipalityResidue. F2 (medição): shared-writer ABBA refutado; janela do missing-consent exclusiva + Onda0 atômico (down transacional) + provision exclusivo. Contraprova: 7 arquivos da key x 6 workers = 12/12 verdes (baseline 1/4).
166 lines
6.5 KiB
TypeScript
166 lines
6.5 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { sql } from '@payloadcms/db-postgres'
|
|
import { getPayload, type Payload } from 'payload'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import { ONDA0_CONSENT_ENTRIES, ONDA0_CONSENT_KEY_LIST } from '@/lib/onda0ConsentTexts'
|
|
import config from '@/payload.config'
|
|
import { requireConsentByKey } from '@/utilities/campaignConsent'
|
|
import {
|
|
provisionOnda0ConsentAndPrivacy,
|
|
provisionOnda0ConsentAndPrivacyDb,
|
|
removeOnda0ConsentAndPrivacyDb,
|
|
} from '@/utilities/onda0Provision'
|
|
|
|
import { assertTestDatabase } from '../helpers/assertTestDatabase'
|
|
import { installCampaignFixtures } from '../helpers/campaignFixtures'
|
|
import {
|
|
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
restoreConsentRows,
|
|
withExclusiveTestDatabaseLease,
|
|
withSharedTestDatabaseLease,
|
|
type ConsentSnapshot,
|
|
} from '../helpers/testDatabaseLease'
|
|
|
|
let payload: Payload
|
|
|
|
installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
describe('Onda 0 provision (integration)', () => {
|
|
beforeAll(async () => {
|
|
assertTestDatabase(process.env.DATABASE_URL)
|
|
payload = await getPayload({ config })
|
|
})
|
|
|
|
it('upserts consent keys and publishes privacy-policy idempotently', async () => {
|
|
// EXCLUSIVE leases on the stable keys other spec files exercise (D10 F2):
|
|
// the provision REWRITES the shared rows' texts, and a consumer that reads
|
|
// the canonical consent between two of those writes would assert against a
|
|
// text that changed under it (observed: campaignInviteUi's preview
|
|
// `requiresConsent`/`consentData` mismatch). Exclusive serializes every
|
|
// consumer for the ~500ms of the provision — bounded, unlike the pre-D10
|
|
// cascade where the ONDA0 exclusive sat behind a queue of EXCLUSIVE
|
|
// writers. The down-test below restores the removed rows with their
|
|
// original ids atomically, so the shared rows are never observed absent.
|
|
await withExclusiveTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, () =>
|
|
withExclusiveTestDatabaseLease(payload, SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, () =>
|
|
withExclusiveTestDatabaseLease(
|
|
payload,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
async () => {
|
|
await provisionOnda0ConsentAndPrivacy(payload)
|
|
await provisionOnda0ConsentAndPrivacy(payload)
|
|
|
|
for (const { key } of ONDA0_CONSENT_ENTRIES) {
|
|
const descriptor = await requireConsentByKey(payload, key)
|
|
expect(descriptor.key).toBe(key)
|
|
expect(descriptor.contentHash).toMatch(/^[a-f0-9]{64}$/)
|
|
}
|
|
|
|
const privacy = await payload.findGlobal({
|
|
slug: 'privacy-policy',
|
|
overrideAccess: true,
|
|
})
|
|
expect(privacy.published).toBe(true)
|
|
expect(privacy.body).toBeTruthy()
|
|
},
|
|
),
|
|
),
|
|
)
|
|
})
|
|
|
|
it('provisionOnda0ConsentAndPrivacyDb upserts and removes via SQL', async () => {
|
|
await withSharedTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, () =>
|
|
withSharedTestDatabaseLease(payload, SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, () =>
|
|
withSharedTestDatabaseLease(
|
|
payload,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
async () => {
|
|
const db = payload.db.drizzle
|
|
|
|
await provisionOnda0ConsentAndPrivacyDb(db)
|
|
await provisionOnda0ConsentAndPrivacyDb(db)
|
|
|
|
const consentRows = await db.execute(sql`
|
|
SELECT "id", "key", "text", "updated_at", "created_at"
|
|
FROM "consent"
|
|
WHERE "key" IN (${sql.join(
|
|
ONDA0_CONSENT_KEY_LIST.map((key) => sql`${key}`),
|
|
sql`, `,
|
|
)})
|
|
ORDER BY "key"
|
|
`)
|
|
expect(consentRows.rows.map((row) => row.key)).toEqual(
|
|
[...ONDA0_CONSENT_KEY_LIST].sort(),
|
|
)
|
|
|
|
const privacyRows = await db.execute(sql`
|
|
SELECT "published", "body"
|
|
FROM "privacy_policy"
|
|
LIMIT 1
|
|
`)
|
|
expect(privacyRows.rows[0]?.published).toBe(true)
|
|
expect(privacyRows.rows[0]?.body).toBeTruthy()
|
|
|
|
// The down-migration must stay invisible to parallel spec files:
|
|
// delete + count + restore run in ONE transaction, so no consumer
|
|
// ever observes the stable keys committed-absent or re-created
|
|
// with a new id (D10 F2 — the non-atomic version flaked load runs
|
|
// with `consent_id` FK violations and fail-closed reads).
|
|
const transactionID = await payload.db.beginTransaction()
|
|
if (transactionID === null) {
|
|
throw new Error('Expected a PostgreSQL transaction for the Onda0 down.')
|
|
}
|
|
const transactionDb = payload.db.sessions?.[String(transactionID)]?.db as
|
|
| typeof db
|
|
| undefined
|
|
if (!transactionDb) {
|
|
await payload.db.rollbackTransaction(transactionID).catch(() => undefined)
|
|
throw new Error('The Onda0 down transaction session is unavailable.')
|
|
}
|
|
try {
|
|
await removeOnda0ConsentAndPrivacyDb(transactionDb)
|
|
|
|
const consentAfterDown = await transactionDb.execute(sql`
|
|
SELECT count(*)::integer AS count
|
|
FROM "consent"
|
|
WHERE "key" IN (${sql.join(
|
|
ONDA0_CONSENT_KEY_LIST.map((key) => sql`${key}`),
|
|
sql`, `,
|
|
)})
|
|
`)
|
|
expect(consentAfterDown.rows[0]?.count).toBe(0)
|
|
|
|
const privacyAfterDown = await transactionDb.execute(sql`
|
|
SELECT "published"
|
|
FROM "privacy_policy"
|
|
LIMIT 1
|
|
`)
|
|
expect(privacyAfterDown.rows[0]?.published).toBe(false)
|
|
|
|
// Restore the removed rows with their ORIGINAL ids inside the
|
|
// same transaction — the commit publishes delete+restore as one
|
|
// atomic state change.
|
|
await restoreConsentRows(
|
|
(query) => transactionDb.execute(query),
|
|
consentRows.rows as ConsentSnapshot[],
|
|
)
|
|
await payload.db.commitTransaction(transactionID)
|
|
} catch (error) {
|
|
await payload.db.rollbackTransaction(transactionID).catch(() => undefined)
|
|
throw error
|
|
}
|
|
},
|
|
),
|
|
),
|
|
)
|
|
})
|
|
})
|