mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
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.
539 lines
18 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|