teqo-backup/tests/int/campaignFixtures.int.spec.ts
Francisco Solla bf25100ed5 chore(OPS45): e2e fixtures — purge de resíduo no claim de município + robustez dos int specs (Closes #723)
O cleanup do campaignE2EFixtures estava completo em runs normais (proxy auto-own
cobre contact/stateDeputy); o lixo vinha de runs crashados, cujo cleanup nunca
roda. O claim e2e agora purga resíduo como o int já fazia, e a purga ganhou a
classe dobradinha (stateDeputy vinculados via municipality.stateDeputies + fichas
órfãs com ref-check incluindo stateDeputy — FK ON DELETE RESTRICT).

peopleList.int.spec.ts passa a varrer todas as páginas (lista global de 25/pág)
com dedupe; personDelete/peopleList usam personName() nos nomes fixos, imunes ao
invariante global de nome de dobradinha.
2026-08-13 14:18:54 -03:00

539 lines
18 KiB
TypeScript

// @vitest-environment node
import { getPayload, type Payload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import type { Consent } from '@/payload-types'
import config from '@/payload.config'
import {
createCampaignFixtures,
purgeMunicipalityResidue,
relationId,
relationIds,
withCampaignFixtures,
} from '../helpers/campaignFixtures'
import { withInviteConsent, withMutableConsentFixture } from '../helpers/testDatabaseLease'
let payload: Payload
const fixtureConsentText: Consent['text'] = {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: 'Consentimento da fixture', version: 1 }],
direction: null,
format: '',
indent: 0,
version: 1,
},
],
direction: null,
format: '',
indent: 0,
version: 1,
},
}
const exists = async (
collection:
| 'users'
| 'campaignUser'
| 'contact'
| 'consent'
| 'leadership'
| 'votePledge'
| 'campaignDemand'
| 'organization'
| 'municipalityUpdate'
| 'supporter',
id: number,
): Promise<boolean> => {
const result = await payload.find({
collection,
where: { id: { equals: id } },
depth: 0,
limit: 1,
})
return result.docs.length === 1
}
describe('campaign integration fixtures', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('cleans a partial setup when creation fails', async () => {
let createdID = 0
await expect(
withCampaignFixtures(payload, async (fixtures) => {
createdID = (await fixtures.createCampaignUser('coordinator')).id
await fixtures.createLeadership({
contact: 999_999_999,
municipalities: [999_999_999],
})
}),
).rejects.toThrow()
expect(await exists('campaignUser', createdID)).toBe(false)
})
it('cleans owned records when the callback fails', async () => {
let createdID = 0
await expect(
withCampaignFixtures(payload, async (fixtures) => {
createdID = (await fixtures.createContact()).id
throw new Error('callback failure')
}),
).rejects.toThrow('callback failure')
expect(await exists('contact', createdID)).toBe(false)
})
it('cleans owned admin users and consent rows when the callback fails', async () => {
let adminID = 0
let consentID = 0
await expect(
withCampaignFixtures(payload, async (fixtures) => {
adminID = (await fixtures.createAdminUser()).id
consentID = (await fixtures.createConsent()).id
throw new Error('callback failure')
}),
).rejects.toThrow('callback failure')
expect(await exists('users', adminID)).toBe(false)
expect(await exists('consent', consentID)).toBe(false)
})
it('cleans the full municipality-scoped graph and resets touched municipality fields', async () => {
let leadershipID = 0
let pledgeID = 0
let demandID = 0
let organizationID = 0
let updateID = 0
let municipalityID = 0
await withCampaignFixtures(payload, async (fixtures) => {
const coordinator = await fixtures.createCampaignUser('coordinator')
const advisor = await fixtures.createCampaignUser('advisor')
const contact = await fixtures.createContact()
const municipality = await fixtures.getMunicipality()
municipalityID = municipality.id
await fixtures.assignMunicipalityAdvisors(municipality, [advisor])
await payload.update({
collection: 'municipality',
id: municipality.id,
data: {
priority: 'alta',
expectedVotes: { pessimistic: 10, central: 50, optimistic: 100 },
strengths: [{ text: fixtures.value('Força') }],
risks: [{ text: fixtures.value('Risco') }],
nextSteps: fixtures.value('Encaminhamento'),
},
depth: 0,
})
const organization = await fixtures.createOrganization({ municipalities: [municipality.id] })
const leadership = await fixtures.createLeadership({
contact,
municipalities: [municipality.id],
organizations: [organization.id],
createdBy: coordinator,
})
const pledge = await fixtures.createVotePledge({
leadership,
municipality,
declaredVotes: 120,
})
const demand = await fixtures.createCampaignDemand({
municipality,
leadership,
createdBy: coordinator,
})
const update = await fixtures.createMunicipalityUpdate({ municipality, author: coordinator })
leadershipID = leadership.id
pledgeID = pledge.id
demandID = demand.id
organizationID = organization.id
updateID = update.id
})
expect(await exists('leadership', leadershipID)).toBe(false)
expect(await exists('votePledge', pledgeID)).toBe(false)
expect(await exists('campaignDemand', demandID)).toBe(false)
expect(await exists('organization', organizationID)).toBe(false)
expect(await exists('municipalityUpdate', updateID)).toBe(false)
const municipality = await payload.findByID({
collection: 'municipality',
id: municipalityID,
depth: 0,
})
expect(municipality.advisors ?? []).toEqual([])
expect(municipality.priority).toBe('normal')
expect(municipality.expectedVotes).toMatchObject({
pessimistic: null,
central: null,
optimistic: null,
})
expect(municipality.strengths ?? []).toEqual([])
expect(municipality.risks ?? []).toEqual([])
expect(municipality.nextSteps ?? null).toBeNull()
expect(municipality.lastUpdateAt ?? null).toBeNull()
})
it('tracks admin users and ordinary consents created through the fixture payload', async () => {
let adminID = 0
let consentID = 0
await withCampaignFixtures(payload, async (fixtures) => {
const admin = await fixtures.payload.create({
collection: 'users',
data: {
email: `${fixtures.value('proxy-admin')}@example.com`,
password: fixtures.value('password'),
roles: ['admin'],
},
depth: 0,
})
const consent = await fixtures.payload.create({
collection: 'consent',
data: {
key: fixtures.value('proxy-consent'),
text: fixtureConsentText,
},
depth: 0,
})
adminID = admin.id
consentID = consent.id
})
expect(await exists('users', adminID)).toBe(false)
expect(await exists('consent', consentID)).toBe(false)
})
it('discovers marked rows created outside the fixture payload', async () => {
const fixtures = createCampaignFixtures(payload)
const contact = await payload.create({
collection: 'contact',
data: {
name: fixtures.value('Contato criado pelo servidor E2E'),
phones: [{ value: fixtures.phone() }],
state: 'BA',
city: 'Salvador',
},
depth: 0,
})
await fixtures.cleanup()
expect(await exists('contact', contact.id)).toBe(false)
await expect(fixtures.expectNoOwnedRows()).resolves.toBeUndefined()
})
it('preserves the stable invite consent unless it is explicitly test-owned', async () => {
await withInviteConsent(payload, async (stableConsent) => {
await withCampaignFixtures(payload, async (fixtures) => {
const reused = await fixtures.payload.findByID({
collection: 'consent',
id: stableConsent.id,
depth: 0,
})
expect(reused.id).toBe(stableConsent.id)
})
expect(await exists('consent', stableConsent.id)).toBe(true)
})
})
it('deletes the stable invite consent when createConsent explicitly owns it', async () => {
let consentID = 0
await withMutableConsentFixture(payload, async (configuredConsent) => {
// The vacating rename uses a PRIVATE key, deliberately OUTSIDE the
// orphan-rename purge list: the canonical row is shared with parallel
// spec files and any key the purge recognizes could be deleted by a
// concurrent fixture's cleanup mid-operation (D10 F2 measurement).
const vacatedKey = `owned-consent-vacated-${configuredConsent.id}`
await payload.update({
collection: 'consent',
id: configuredConsent.id,
data: { key: vacatedKey },
depth: 0,
})
await withCampaignFixtures(payload, async (fixtures) => {
consentID = (
await fixtures.createConsent({
key: 'lideranca-autopreenchimento',
})
).id
})
expect(await exists('consent', consentID)).toBe(false)
await payload.update({
collection: 'consent',
id: configuredConsent.id,
data: { key: 'lideranca-autopreenchimento' },
depth: 0,
})
})
})
// Fixed sentinel phones can survive an aborted previous run — reuse them.
const findOrCreateSentinelContact = async (name: string, phone: string) => {
const existing = await payload.find({
collection: 'contact',
where: { 'phones.value': { equals: phone } },
depth: 0,
limit: 1,
})
if (existing.docs[0]) return existing.docs[0]
return payload.create({
collection: 'contact',
data: { name, phones: [{ value: phone }], state: 'BA', city: 'Salvador' },
depth: 0,
})
}
it('preserves unrelated sentinel rows', async () => {
const sentinel = await findOrCreateSentinelContact('Sentinela não pertencente', '71900000001')
try {
await withCampaignFixtures(payload, async (fixtures) => {
await fixtures.createContact()
})
expect(await exists('contact', sentinel.id)).toBe(true)
} finally {
await payload.delete({ collection: 'contact', id: sentinel.id, depth: 0 })
}
})
it('preserves a pre-existing contact reused by an owned leadership', async () => {
const sentinel = await findOrCreateSentinelContact(
'Contato existente reutilizado',
'71900000002',
)
// Leftover leadership from an aborted run would violate the unique contact.
await payload.delete({
collection: 'leadership',
where: { contact: { equals: sentinel.id } },
depth: 0,
})
try {
await withCampaignFixtures(payload, async (fixtures) => {
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
await fixtures.createLeadership({
contact: sentinel,
municipalities: [municipality.id],
createdBy: coordinator,
})
})
expect(await exists('contact', sentinel.id)).toBe(true)
} finally {
await payload.delete({ collection: 'contact', id: sentinel.id, depth: 0 })
}
})
it('preserves unrelated admin-user and consent sentinels', async () => {
const marker = `${process.pid}-${Date.now()}`
const admin = await payload.create({
collection: 'users',
data: {
email: `sentinel-${marker}@example.com`,
password: `password-${marker}`,
roles: ['admin'],
},
depth: 0,
})
const consent = await payload.create({
collection: 'consent',
data: {
key: `sentinel-${marker}`,
text: fixtureConsentText,
},
depth: 0,
})
try {
await withCampaignFixtures(payload, async (fixtures) => {
await fixtures.createAdminUser()
await fixtures.createConsent()
})
expect(await exists('users', admin.id)).toBe(true)
expect(await exists('consent', consent.id)).toBe(true)
} finally {
await payload.delete({ collection: 'users', id: admin.id, depth: 0 })
await payload.delete({ collection: 'consent', id: consent.id, depth: 0 })
}
})
it('hands out distinct seeded municipalities to isolated parallel builders', async () => {
const first = createCampaignFixtures(payload)
const second = createCampaignFixtures(payload)
const [firstMunicipality, secondMunicipality, firstContact, secondContact] = await Promise.all([
first.getMunicipality(),
second.getMunicipality(),
first.createContact(),
second.createContact(),
])
expect(first.runID).not.toBe(second.runID)
expect(firstMunicipality.id).not.toBe(secondMunicipality.id)
await first.cleanup()
expect(await exists('contact', firstContact.id)).toBe(false)
expect(await exists('contact', secondContact.id)).toBe(true)
await second.cleanup()
expect(await exists('contact', secondContact.id)).toBe(false)
})
it('makes cleanup idempotent and leaves zero owned rows', async () => {
const fixtures = createCampaignFixtures(payload)
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
await fixtures.assignMunicipalityAdvisors(municipality, [coordinator])
await fixtures.cleanup()
await fixtures.cleanup()
await expect(fixtures.expectNoOwnedRows()).resolves.toBeUndefined()
const persistedMunicipality = await payload.findByID({
collection: 'municipality',
id: municipality.id,
depth: 0,
})
expect(persistedMunicipality.advisors ?? []).toEqual([])
})
it('purges residue supporters and the contacts they orphaned on a municipality claim', async () => {
// D10 F1: an aborted run leaves supporters AND their contact rows on the
// claimed municipality. The purge must delete the orphaned contacts (no
// other join references them) while preserving contacts that a live
// leadership outside the purged municipality still references.
const fixtures = createCampaignFixtures(payload)
try {
const municipality = await fixtures.getMunicipality()
const coordinator = await fixtures.createCampaignUser('coordinator')
const orphanContact = await fixtures.createContact()
const residueSupporter = await fixtures.createSupporter({
contact: orphanContact,
municipality,
createdBy: coordinator,
})
const preservedContact = await fixtures.createContact()
await fixtures.createSupporter({
contact: preservedContact,
municipality,
createdBy: coordinator,
})
const otherMunicipality = await fixtures.getMunicipality()
await fixtures.createLeadership({
contact: preservedContact,
municipalities: [otherMunicipality.id],
createdBy: coordinator,
})
await purgeMunicipalityResidue(payload, municipality.id)
expect(await exists('supporter', residueSupporter.id)).toBe(false)
expect(await exists('contact', orphanContact.id)).toBe(false)
expect(await exists('contact', preservedContact.id)).toBe(true)
} finally {
await fixtures.cleanup()
}
})
it('purges residue dobradinhas and their orphaned fichas on a municipality claim', async () => {
// OPS45: a crashed run leaves deputies linked through the claimed
// municipality's `stateDeputies`. The purge must delete the deputies and
// the fichas only a residue deputy referenced, clear the field, and
// preserve fichas that a live join still references.
const fixtures = createCampaignFixtures(payload)
try {
const municipality = await fixtures.getMunicipality()
const orphanDeputy = await fixtures.createStateDeputy()
const preservedContact = await fixtures.createContact()
const otherMunicipality = await fixtures.getMunicipality()
await fixtures.createLeadership({
contact: preservedContact,
municipalities: [otherMunicipality.id],
})
const preservedDeputy = await fixtures.createStateDeputy({ contact: preservedContact })
await payload.update({
collection: 'municipality',
id: municipality.id,
data: { stateDeputies: [orphanDeputy.id, preservedDeputy.id] },
depth: 0,
overrideAccess: true,
})
await purgeMunicipalityResidue(payload, municipality.id)
const deputyExists = async (id: number): Promise<boolean> => {
const result = await payload.find({
collection: 'stateDeputy',
where: { id: { equals: id } },
depth: 0,
limit: 1,
overrideAccess: true,
})
return result.docs.length === 1
}
expect(await deputyExists(orphanDeputy.id)).toBe(false)
expect(await deputyExists(preservedDeputy.id)).toBe(false)
expect(await exists('contact', relationId(orphanDeputy.contact))).toBe(false)
expect(await exists('contact', preservedContact.id)).toBe(true)
const persistedMunicipality = await payload.findByID({
collection: 'municipality',
id: municipality.id,
depth: 0,
overrideAccess: true,
})
expect(relationIds(persistedMunicipality.stateDeputies ?? [])).toEqual([])
} finally {
await fixtures.cleanup()
}
})
it('purges a shared ficha only after every joiner is gone (supporter + dobradinha)', async () => {
// OPS45 — the residue shape that crashed the purge: ONE ficha shared by a
// dobradinha (linked to municipality A) and a supporter (on municipality
// B), no account in between. The supporter purge must not delete the
// ficha while the deputy still references it (`stateDeputy.contact` is
// ON DELETE RESTRICT — deleting would throw), and the later claim of A
// must finish the job.
const fixtures = createCampaignFixtures(payload)
try {
const deputyMunicipality = await fixtures.getMunicipality()
const sharedContact = await fixtures.createContact()
const deputy = await fixtures.createStateDeputy({ contact: sharedContact })
await payload.update({
collection: 'municipality',
id: deputyMunicipality.id,
data: { stateDeputies: [deputy.id] },
depth: 0,
overrideAccess: true,
})
const supporterMunicipality = await fixtures.getMunicipality()
const supporter = await fixtures.createSupporter({
contact: sharedContact,
municipality: supporterMunicipality,
})
await purgeMunicipalityResidue(payload, supporterMunicipality.id)
expect(await exists('supporter', supporter.id)).toBe(false)
expect(await exists('contact', sharedContact.id)).toBe(true)
await purgeMunicipalityResidue(payload, deputyMunicipality.id)
expect(await exists('contact', sharedContact.id)).toBe(false)
} finally {
await fixtures.cleanup()
}
})
})