mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
- campaignSupporter: destaca voteIntentionConsent dos supporters antes de purgar o consent canônico de intenção de voto — o seed mínimo cria vínculos vivos nessa chave, e o delete passou a depender do SET NULL silencioso que o guard agora bloqueia (falha determinística em DB fresca de CI). - campaignInvite: purga da chave lideranca-autopreenchimento na janela mutável passa a raw SQL (precedente Onda0/lease) — lifecycle de harness nunca deve passar pelo caminho admin/beforeDelete.
667 lines
23 KiB
TypeScript
667 lines
23 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { getPayload, type Payload } from 'payload'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
confirmSupporterImportRecord,
|
|
createSupporterRecord,
|
|
previewSupporterImportText,
|
|
removeSupporterDataRecord,
|
|
setSupporterVoteIntentionRecord,
|
|
} from '@/app/(campaign)/campanha/actions/supporter'
|
|
import {
|
|
SUPPORTER_IMPORT_BATCH_EMPTY_MESSAGE,
|
|
supporterCreateSchema,
|
|
} from '@/lib/schemas/supporter'
|
|
import config from '@/payload.config'
|
|
import {
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
|
} from '@/utilities/campaignConsent'
|
|
import {
|
|
isPreviewErrorRow,
|
|
SUPPORTER_IMPORT_SHARED_PHONE_MESSAGE,
|
|
} from '@/utilities/supporter/supporterImport'
|
|
import {
|
|
loadSupporterListOverviewData,
|
|
loadSupportersPageData,
|
|
} from '@/utilities/supporter/supporterPageData'
|
|
import { buildSupporterListWhere } from '@/utilities/supporter/supporterUi'
|
|
|
|
import { installCampaignFixtures, relationId } from '../helpers/campaignFixtures'
|
|
import {
|
|
ensureLeasedConsent,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
withMissingInviteConsentFixture,
|
|
} from '../helpers/testDatabaseLease'
|
|
|
|
let payload: Payload
|
|
const campaignFixtures = installCampaignFixtures({
|
|
getPayload: () => payload,
|
|
setPayload: (nextPayload) => {
|
|
payload = nextPayload
|
|
},
|
|
})
|
|
|
|
/**
|
|
* Shared consent rows (stable keys) are leased fixtures: ensured under an
|
|
* advisory-lock lease and never owned/deleted, so parallel spec files can rely
|
|
* on them (see `leasedConsentKeys` in campaignFixtures.ts).
|
|
*/
|
|
const ensureConsentByKey = (key: string, leaseKey: string) =>
|
|
ensureLeasedConsent(payload, { consentKey: key, leaseKey })
|
|
|
|
const ensureSupporterConsents = async () => {
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const voteIntention = await ensureConsentByKey(
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
)
|
|
return { registration, voteIntention }
|
|
}
|
|
|
|
describe('campaign supporter domain', () => {
|
|
beforeAll(async () => {
|
|
payload = await getPayload({ config: await config })
|
|
})
|
|
|
|
it('requires phone and registration consent on create schema', () => {
|
|
expect(
|
|
supporterCreateSchema.safeParse({
|
|
name: 'Maria Silva',
|
|
phones: ['71999990000'],
|
|
consentAccepted: true,
|
|
}).success,
|
|
).toBe(true)
|
|
|
|
expect(
|
|
supporterCreateSchema.safeParse({
|
|
name: 'Maria Silva',
|
|
phones: ['71999990000'],
|
|
}).success,
|
|
).toBe(false)
|
|
|
|
expect(
|
|
supporterCreateSchema.safeParse({
|
|
name: 'Maria Silva',
|
|
phones: ['invalid'],
|
|
consentAccepted: true,
|
|
}).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('fails closed when supporter registration consent is missing', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
|
|
await withMissingInviteConsentFixture(
|
|
payload,
|
|
async () => {
|
|
await expect(
|
|
createSupporterRecord(payload, coordinator, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [fixtures.phone()],
|
|
consentAccepted: true,
|
|
}),
|
|
).rejects.toThrow(/Consentimento de cadastro de apoiador ainda não configurado/)
|
|
},
|
|
{
|
|
consentKey: SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
leaseKey: SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
},
|
|
)
|
|
})
|
|
|
|
it('creates a supporter with contact upsert and blocks leadership coexistence', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const municipality = await fixtures.getMunicipality()
|
|
const phone = fixtures.phone()
|
|
|
|
const created = await createSupporterRecord(payload, coordinator, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [phone],
|
|
city: 'Salvador',
|
|
consentAccepted: true,
|
|
})
|
|
fixtures.own('supporter', created.id)
|
|
fixtures.own('contact', created.contact)
|
|
|
|
expect(created.contactReused).toBe(false)
|
|
expect(created.source).toBe('manual')
|
|
|
|
const contact = await fixtures.createContact()
|
|
await fixtures.createLeadership({
|
|
contact: contact.id,
|
|
municipalities: [municipality.id],
|
|
createdBy: coordinator.id,
|
|
})
|
|
|
|
await expect(
|
|
createSupporterRecord(payload, coordinator, {
|
|
name: contact.name,
|
|
phones: [contact.phone],
|
|
municipality: municipality.id,
|
|
consentAccepted: true,
|
|
}),
|
|
).rejects.toThrow(/já é liderança neste município/)
|
|
})
|
|
|
|
it('scopes advisor create to administered municipalities and blocks municipality-less create', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const municipality = await fixtures.getMunicipality()
|
|
const otherMunicipality = await fixtures.getMunicipality()
|
|
await fixtures.assignMunicipalityAdvisors(municipality, [advisor])
|
|
|
|
await expect(
|
|
createSupporterRecord(payload, advisor, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [fixtures.phone()],
|
|
consentAccepted: true,
|
|
}),
|
|
).rejects.toThrow(/sem município/)
|
|
|
|
await expect(
|
|
createSupporterRecord(payload, advisor, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [fixtures.phone()],
|
|
municipality: otherMunicipality.id,
|
|
consentAccepted: true,
|
|
}),
|
|
).rejects.toThrow()
|
|
|
|
const created = await createSupporterRecord(payload, advisor, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [fixtures.phone()],
|
|
municipality: municipality.id,
|
|
consentAccepted: true,
|
|
})
|
|
fixtures.own('supporter', created.id)
|
|
fixtures.own('contact', created.contact)
|
|
expect(created.id).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('requires highlighted vote-intention consent before setting intention', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const contact = await fixtures.createContact()
|
|
const supporter = await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: coordinator.id,
|
|
})
|
|
|
|
const existingVoteConsent = await payload.find({
|
|
collection: 'consent',
|
|
where: { key: { equals: SUPPORTER_VOTE_INTENTION_CONSENT_KEY } },
|
|
depth: 0,
|
|
limit: 1,
|
|
})
|
|
if (existingVoteConsent.docs[0]) {
|
|
// S9+: Consent.beforeDelete refuses deleting an in-use legal text. The
|
|
// minimal seed links seeded supporters to this canonical consent, so
|
|
// detach the live voteIntentionConsent references FIRST — the explicit,
|
|
// ordered version of the FK `ON DELETE SET NULL` this purge relied on
|
|
// silently before the guard existed.
|
|
await payload.update({
|
|
collection: 'supporter',
|
|
where: { voteIntentionConsent: { equals: existingVoteConsent.docs[0].id } },
|
|
data: { voteIntentionConsent: null },
|
|
overrideAccess: true,
|
|
})
|
|
await payload.delete({
|
|
collection: 'consent',
|
|
id: existingVoteConsent.docs[0].id,
|
|
})
|
|
}
|
|
|
|
await expect(
|
|
setSupporterVoteIntentionRecord(payload, coordinator, {
|
|
id: supporter.id,
|
|
voteIntention: 'certo',
|
|
voteIntentionConsentAccepted: true,
|
|
}),
|
|
).rejects.toThrow(/Consentimento de intenção de voto ainda não configurado/)
|
|
|
|
await ensureConsentByKey(
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
|
SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY,
|
|
)
|
|
|
|
const updated = await setSupporterVoteIntentionRecord(payload, coordinator, {
|
|
id: supporter.id,
|
|
voteIntention: 'certo',
|
|
voteIntentionConsentAccepted: true,
|
|
})
|
|
expect(updated.voteIntention).toBe('certo')
|
|
expect(updated.voteIntentionConsentedAt).toBeTruthy()
|
|
})
|
|
|
|
it('previews and confirms CSV import for the coordinator only', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const phoneOk = fixtures.phone()
|
|
const phoneDup = fixtures.phone()
|
|
const existing = await fixtures.createContact({ phones: [{ value: phoneDup }] })
|
|
await fixtures.createSupporter({
|
|
contact: existing.id,
|
|
createdBy: coordinator.id,
|
|
})
|
|
|
|
const csv = [
|
|
'nome,telefone,municipio,intencao',
|
|
`Ana Silva,${phoneOk},Salvador,certo`,
|
|
`Bruno Costa,${phoneDup},Salvador,indeciso`,
|
|
'Carla,119999,Salvador,certo',
|
|
'Diego Souza,71988887777,Cidade Inventada,certo',
|
|
].join('\n')
|
|
|
|
await expect(previewSupporterImportText(payload, advisor, csv)).rejects.toThrow(
|
|
/Coordenador Geral/,
|
|
)
|
|
|
|
const preview = await previewSupporterImportText(payload, coordinator, csv)
|
|
expect(preview.counts.ok).toBe(1)
|
|
expect(preview.counts.duplicate).toBe(1)
|
|
expect(preview.counts.error).toBeGreaterThanOrEqual(2)
|
|
expect(preview.importToken).toBeTruthy()
|
|
expect(preview.sampleRows.length).toBeGreaterThan(0)
|
|
|
|
const result = await confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
})
|
|
expect(result.created).toBe(1)
|
|
|
|
// Single-use token: a second confirm with the same token must fail.
|
|
await expect(
|
|
confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
}),
|
|
).rejects.toThrow(/não encontrado|expirado|inválido/)
|
|
|
|
const contact = await payload.find({
|
|
collection: 'contact',
|
|
where: { 'phones.value': { equals: phoneOk } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
expect(contact.totalDocs).toBe(1)
|
|
fixtures.own('contact', contact.docs[0]!.id)
|
|
|
|
const created = await payload.find({
|
|
collection: 'supporter',
|
|
where: { contact: { equals: contact.docs[0]!.id } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
expect(created.totalDocs).toBe(1)
|
|
fixtures.own('supporter', created.docs[0]!.id)
|
|
})
|
|
|
|
it('flags a CSV phone shared by two fichas for manual resolution (C111)', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const sharedPhone = fixtures.phone()
|
|
await fixtures.createContact({ name: 'Comitê Central', phones: [{ value: sharedPhone }] })
|
|
await fixtures.createContact({ name: 'Ana', phones: [{ value: sharedPhone }] })
|
|
|
|
const csv = [
|
|
'nome,telefone,municipio,intencao',
|
|
`Maria Souza,${sharedPhone},Salvador,certo`,
|
|
].join('\n')
|
|
|
|
const preview = await previewSupporterImportText(payload, coordinator, csv)
|
|
const row = preview.sampleRows[0]
|
|
expect(row).toBeDefined()
|
|
expect(row.status).toBe('telefone_compartilhado')
|
|
expect(isPreviewErrorRow(row)).toBe(true)
|
|
|
|
// The shared-phone row never reaches the staged ok set: confirming an
|
|
// all-flagged batch fails closed instead of importing a guessed ficha.
|
|
await expect(
|
|
confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
}),
|
|
).rejects.toThrow(SUPPORTER_IMPORT_BATCH_EMPTY_MESSAGE)
|
|
})
|
|
|
|
it('aborts the confirm fail-closed when the base gains a second ficha for an ok phone (C111)', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const phone = fixtures.phone()
|
|
await fixtures.createContact({ phones: [{ value: phone }] })
|
|
|
|
const csv = `nome,telefone,municipio,intencao\nAna Silva,${phone},Salvador,certo`
|
|
const preview = await previewSupporterImportText(payload, coordinator, csv)
|
|
expect(preview.counts.ok).toBe(1)
|
|
|
|
// Simulate the race: between preview and confirm another ficha claims the
|
|
// same phone. The confirm holds the phone locks and must fail closed.
|
|
await fixtures.createContact({ name: 'Segunda Ficha', phones: [{ value: phone }] })
|
|
|
|
await expect(
|
|
confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
}),
|
|
).rejects.toThrow(SUPPORTER_IMPORT_SHARED_PHONE_MESSAGE)
|
|
})
|
|
|
|
it('rejects a tampered import token and a token issued to another actor', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const otherCoordinator = await fixtures.createCampaignUser('coordinator')
|
|
const phoneOk = fixtures.phone()
|
|
const csv = `nome,telefone,municipio,intencao\nAna Silva,${phoneOk},Salvador,certo`
|
|
|
|
const preview = await previewSupporterImportText(payload, coordinator, csv)
|
|
expect(preview.counts.ok).toBe(1)
|
|
|
|
const [batchId, expiresAt, sig] = preview.importToken.split('.')
|
|
const tampered = `${batchId}.${expiresAt}.${sig.slice(0, -2)}xx`
|
|
await expect(
|
|
confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: tampered,
|
|
}),
|
|
).rejects.toThrow(/inválido|expirado/)
|
|
|
|
// Token bound to a different actor cannot be redeemed by another coordinator.
|
|
await expect(
|
|
confirmSupporterImportRecord(payload, otherCoordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
}),
|
|
).rejects.toThrow(/não encontrado|inválido|expirado/)
|
|
|
|
// Cleanup: consume the still-valid token so it does not leak into other tests.
|
|
await confirmSupporterImportRecord(payload, coordinator, {
|
|
operatorAttested: true,
|
|
importToken: preview.importToken,
|
|
})
|
|
const contact = await payload.find({
|
|
collection: 'contact',
|
|
where: { 'phones.value': { equals: phoneOk } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
fixtures.own('contact', contact.docs[0]!.id)
|
|
const created = await payload.find({
|
|
collection: 'supporter',
|
|
where: { contact: { equals: contact.docs[0]!.id } },
|
|
depth: 0,
|
|
limit: 1,
|
|
overrideAccess: true,
|
|
})
|
|
if (created.docs[0]) fixtures.own('supporter', created.docs[0].id)
|
|
})
|
|
|
|
it('removes supporter and anonymizes contact when no other joins remain', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const phone = fixtures.phone()
|
|
const created = await createSupporterRecord(payload, coordinator, {
|
|
name: fixtures.value('Apoiador'),
|
|
phones: [phone],
|
|
consentAccepted: true,
|
|
})
|
|
const contactID = relationId(created.contact)
|
|
fixtures.own('supporter', created.id)
|
|
fixtures.own('contact', contactID)
|
|
|
|
const removed = await removeSupporterDataRecord(payload, coordinator, { id: created.id })
|
|
expect(removed.removed).toBe(true)
|
|
expect(removed.contactAnonymized).toBe(true)
|
|
|
|
const contact = await payload.findByID({
|
|
collection: 'contact',
|
|
id: contactID,
|
|
depth: 0,
|
|
})
|
|
expect(contact.name).toBe('Titular removido')
|
|
expect(contact.phones?.map((entry) => entry.value)).not.toContain(phone)
|
|
})
|
|
|
|
it('aggregates supporter overview KPIs in a single SQL query for the coordinator', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
|
|
const intentions: Array<{
|
|
voteIntention: 'certo' | 'tende_a_certo' | 'indeciso' | 'outro' | null
|
|
}> = [
|
|
{ voteIntention: 'certo' },
|
|
{ voteIntention: 'certo' },
|
|
{ voteIntention: 'tende_a_certo' },
|
|
{ voteIntention: 'indeciso' },
|
|
{ voteIntention: 'indeciso' },
|
|
{ voteIntention: 'outro' },
|
|
{ voteIntention: null },
|
|
]
|
|
const searchTag = fixtures.value('OverviewKpi')
|
|
for (const { voteIntention } of intentions) {
|
|
const contact = await fixtures.createContact({ name: searchTag })
|
|
await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: coordinator.id,
|
|
...(voteIntention ? { voteIntention } : {}),
|
|
})
|
|
}
|
|
|
|
const list = await payload.find({
|
|
collection: 'supporter',
|
|
where: buildSupporterListWhere({ page: 1, q: searchTag }),
|
|
depth: 0,
|
|
pagination: false,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
const overview = await loadSupporterListOverviewData(
|
|
payload,
|
|
coordinator,
|
|
{ page: 1, q: searchTag },
|
|
list.totalDocs,
|
|
)
|
|
expect(overview).not.toBeNull()
|
|
expect(overview!.total).toBe(intentions.length)
|
|
expect(overview!.certoAndTende).toBe(3)
|
|
expect(overview!.indeciso).toBe(2)
|
|
|
|
// `total` is the caller's contract: a 0 total skips the aggregate query
|
|
// entirely and hides the panel, even if rows technically exist.
|
|
expect(await loadSupporterListOverviewData(payload, coordinator, { page: 1 }, 0)).toBeNull()
|
|
})
|
|
|
|
it('scopes the overview aggregate to administered municipalities for an advisor and applies filters', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const assigned = await fixtures.getMunicipality()
|
|
const other = await fixtures.getMunicipality()
|
|
await fixtures.assignMunicipalityAdvisors(assigned, [advisor])
|
|
|
|
const makeSupporter = async (
|
|
municipalityId: number | undefined,
|
|
voteIntention: 'certo' | 'indeciso' | null,
|
|
city = 'Salvador',
|
|
name = fixtures.value('Apoiador'),
|
|
) => {
|
|
const contact = await fixtures.createContact({ city, name })
|
|
await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: advisor.id,
|
|
...(municipalityId ? { municipality: municipalityId } : {}),
|
|
...(voteIntention ? { voteIntention } : {}),
|
|
})
|
|
}
|
|
|
|
await makeSupporter(assigned.id, 'certo')
|
|
await makeSupporter(assigned.id, 'indeciso')
|
|
await makeSupporter(assigned.id, null, 'Feira de Santana')
|
|
await makeSupporter(other.id, 'certo') // outside scope — must not be counted
|
|
|
|
const overview = await loadSupporterListOverviewData(payload, advisor, { page: 1 }, 3)
|
|
expect(overview).not.toBeNull()
|
|
expect(overview!.total).toBe(3)
|
|
expect(overview!.certoAndTende).toBe(1)
|
|
expect(overview!.indeciso).toBe(1)
|
|
|
|
const filtered = await loadSupporterListOverviewData(
|
|
payload,
|
|
advisor,
|
|
{ page: 1, city: 'Feira de Santana' },
|
|
1,
|
|
)
|
|
expect(filtered).not.toBeNull()
|
|
expect(filtered!.total).toBe(1)
|
|
})
|
|
|
|
it('ignores single-character search queries in list and overview filters', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
|
|
const uniqueName = fixtures.value('ApoiadorUnico')
|
|
const contact = await fixtures.createContact({ name: uniqueName })
|
|
await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: coordinator.id,
|
|
})
|
|
|
|
const shortQuery = uniqueName.slice(0, 1)
|
|
const listWhere = buildSupporterListWhere({ page: 1, q: shortQuery })
|
|
const list = await payload.find({
|
|
collection: 'supporter',
|
|
where: listWhere,
|
|
depth: 0,
|
|
pagination: false,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(list.totalDocs).toBeGreaterThanOrEqual(1)
|
|
|
|
const overview = await loadSupporterListOverviewData(
|
|
payload,
|
|
coordinator,
|
|
{ page: 1, q: shortQuery },
|
|
list.totalDocs,
|
|
)
|
|
expect(overview).not.toBeNull()
|
|
expect(overview!.total).toBe(list.totalDocs)
|
|
})
|
|
|
|
it('keeps list and overview totals aligned for multi-character search', async () => {
|
|
const fixtures = campaignFixtures()
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
|
|
const uniqueName = fixtures.value('ApoiadorBusca')
|
|
const contact = await fixtures.createContact({ name: uniqueName })
|
|
await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: coordinator.id,
|
|
})
|
|
|
|
const q = uniqueName.slice(0, 8)
|
|
const list = await payload.find({
|
|
collection: 'supporter',
|
|
where: buildSupporterListWhere({ page: 1, q }),
|
|
depth: 0,
|
|
pagination: false,
|
|
user: coordinator,
|
|
overrideAccess: false,
|
|
})
|
|
|
|
expect(list.totalDocs).toBeGreaterThanOrEqual(1)
|
|
|
|
const overview = await loadSupporterListOverviewData(
|
|
payload,
|
|
coordinator,
|
|
{ page: 1, q },
|
|
list.totalDocs,
|
|
)
|
|
expect(overview).not.toBeNull()
|
|
expect(overview!.total).toBe(list.totalDocs)
|
|
expect(overview!.certoAndTende + overview!.indeciso).toBeLessThanOrEqual(list.totalDocs)
|
|
})
|
|
|
|
it('loads supporters page data scoped to the administered municipalities of an advisor', async () => {
|
|
const fixtures = campaignFixtures()
|
|
await ensureSupporterConsents()
|
|
const advisor = await fixtures.createCampaignUser('advisor')
|
|
const assigned = await fixtures.getMunicipality()
|
|
await fixtures.assignMunicipalityAdvisors(assigned, [advisor])
|
|
const registration = await ensureConsentByKey(
|
|
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
|
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
|
|
)
|
|
|
|
const contact = await fixtures.createContact()
|
|
await fixtures.createSupporter({
|
|
contact: contact.id,
|
|
municipality: assigned.id,
|
|
consent: registration.id,
|
|
consentContentHash: 'hash',
|
|
consentedAt: new Date().toISOString(),
|
|
createdBy: advisor.id,
|
|
})
|
|
|
|
const pageData = await loadSupportersPageData(payload, advisor, {})
|
|
expect(pageData.advisorMunicipalityIds).toContain(assigned.id)
|
|
expect(pageData.municipalityOptions.map((option) => option.id)).toEqual([assigned.id])
|
|
expect(pageData.result.totalDocs).toBeGreaterThanOrEqual(1)
|
|
expect(pageData.overview).not.toBeNull()
|
|
})
|
|
})
|