teqo/tests/int/campaignCoordinatorAssignment.int.spec.ts
Francisco Ignácio Fontoura Solla bcac665cb5 Ship /campanha nuclei MVP with RBAC, invites, and vote estimates.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.
2026-07-18 15:01:56 -03:00

219 lines
8.7 KiB
TypeScript

// @vitest-environment node
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { getPayload, type Payload } from 'payload'
import { assignNucleusCoordinatorsRecord } from '@/app/(campaign)/campanha/actions/coordinatorAssignment'
import { updateElectoralNucleus } from '@/app/(campaign)/campanha/actions/nucleus'
import config from '@/payload.config'
import { getNucleusCoordinatorAssignmentPageData } from '@/utilities/nucleusCoordinatorAssignmentPageData'
import { resolveAccessibleNucleusContext } from '@/utilities/nucleusPageData'
import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks'
import {
getTestTransactionBackendPID,
waitForAdvisoryLockWaiter,
} from '../helpers/testDatabaseLease'
import { withCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
describe('campaign coordinator assignment', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('lets only fresh general coordination replace assignments with eligible users', async () => {
await withCampaignFixtures(payload, async (fixtures) => {
const general = await fixtures.createCampaignUser('geral')
const coordinator = await fixtures.createCampaignUser('coordenador')
const replacement = await fixtures.createCampaignUser('coordenador')
const leadership = await fixtures.createCampaignUser('lideranca')
const nucleus = await fixtures.createNucleus({ coordinators: [coordinator.id] })
const updated = await assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [general.id, replacement.id],
expectedUpdatedAt: nucleus.updatedAt,
})
expect(updated.coordinators).toEqual([general.id, replacement.id])
await expect(
assignNucleusCoordinatorsRecord(payload, coordinator, {
slug: nucleus.slug,
coordinatorIds: [coordinator.id],
expectedUpdatedAt: updated.updatedAt,
}),
).rejects.toThrow('coordenação geral')
await expect(
assignNucleusCoordinatorsRecord(payload, leadership, {
slug: nucleus.slug,
coordinatorIds: [],
expectedUpdatedAt: updated.updatedAt,
}),
).rejects.toThrow('coordenação geral')
await expect(
assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [leadership.id],
expectedUpdatedAt: updated.updatedAt,
}),
).rejects.toThrow('elegíveis')
})
})
it('rejects stale dialogs under the nucleus lock without overwriting newer assignments', async () => {
await withCampaignFixtures(payload, async (fixtures) => {
const general = await fixtures.createCampaignUser('geral')
const first = await fixtures.createCampaignUser('coordenador')
const second = await fixtures.createCampaignUser('coordenador')
const nucleus = await fixtures.createNucleus()
const firstUpdate = await assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [first.id],
expectedUpdatedAt: nucleus.updatedAt,
})
await expect(
assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [second.id],
expectedUpdatedAt: nucleus.updatedAt,
}),
).rejects.toThrow('alterada')
const persisted = await payload.findByID({
collection: 'electoralNucleus',
id: nucleus.id,
depth: 0,
})
expect(persisted.coordinators).toEqual([first.id])
expect(persisted.updatedAt).toBe(firstUpdate.updatedAt)
})
})
it('waits on the exact coordinator-assignment namespace before revision checks', async () => {
await withCampaignFixtures(payload, async (fixtures) => {
const general = await fixtures.createCampaignUser('geral')
const coordinator = await fixtures.createCampaignUser('coordenador')
const nucleus = await fixtures.createNucleus()
const holderID = await payload.db.beginTransaction()
if (holderID === null) throw new Error('Expected a PostgreSQL transaction.')
await acquireTextAdvisoryLocks(payload, { transactionID: holderID }, [
`coordinator-assignment:${nucleus.id}`,
])
const originalBegin = payload.db.beginTransaction.bind(payload.db)
let resolveWaiterPID!: (pid: number) => void
const waiterPID = new Promise<number>((resolve) => {
resolveWaiterPID = resolve
})
const beginSpy = vi.spyOn(payload.db, 'beginTransaction').mockImplementation(async () => {
const transactionID = await originalBegin()
if (transactionID === null) throw new Error('Expected assignment transaction.')
resolveWaiterPID(await getTestTransactionBackendPID(payload, transactionID))
return transactionID
})
const pending = assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [coordinator.id],
expectedUpdatedAt: nucleus.updatedAt,
})
try {
const expectedPID = await waiterPID
await expect(
waitForAdvisoryLockWaiter(payload, {
key: `coordinator-assignment:${nucleus.id}`,
mode: 'ExclusiveLock',
waiterPID: expectedPID,
}),
).resolves.toMatchObject({ granted: false, pid: expectedPID })
await payload.db.rollbackTransaction(holderID)
await expect(pending).resolves.toMatchObject({ coordinators: [coordinator.id] })
} catch (error) {
await payload.db.rollbackTransaction(holderID).catch(() => undefined)
throw error
} finally {
beginSpy.mockRestore()
}
})
})
it('does not lose a dedicated assignment when a generic crafted update runs concurrently', async () => {
await withCampaignFixtures(payload, async (fixtures) => {
const general = await fixtures.createCampaignUser('geral')
const coordinator = await fixtures.createCampaignUser('coordenador')
const replacement = await fixtures.createCampaignUser('coordenador')
const nucleus = await fixtures.createNucleus({ coordinators: [coordinator.id] })
const [assignmentResult, genericResult] = await Promise.allSettled([
assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [replacement.id],
expectedUpdatedAt: nucleus.updatedAt,
}),
updateElectoralNucleus(payload, general, {
id: nucleus.id,
coordinators: [coordinator.id],
territoryNotes: 'Atualização concorrente',
} as never),
])
expect(genericResult.status).toBe('fulfilled')
let persisted = await payload.findByID({
collection: 'electoralNucleus',
id: nucleus.id,
depth: 0,
})
if (assignmentResult.status === 'rejected') {
expect(assignmentResult.reason).toEqual(
expect.objectContaining({ message: expect.any(String) }),
)
persisted = await assignNucleusCoordinatorsRecord(payload, general, {
slug: nucleus.slug,
coordinatorIds: [replacement.id],
expectedUpdatedAt: persisted.updatedAt,
})
}
expect(persisted.coordinators).toEqual([replacement.id])
expect(persisted.territoryNotes).toBe('Atualização concorrente')
})
})
it('returns a minimal scoped DTO with dedicated phones but no auth identity fields', async () => {
await withCampaignFixtures(payload, async (fixtures) => {
const general = await fixtures.createCampaignUser('geral')
const phone = fixtures.phone()
const coordinator = await fixtures.createCampaignUser('coordenador', { phone })
const leader = await fixtures.createCampaignUser('lideranca')
const nucleus = await fixtures.createNucleus({
coordinators: [general.id, coordinator.id],
})
const contact = await fixtures.createContact({
name: fixtures.value('Liderança'),
phone: fixtures.phone(),
})
await fixtures.createLeadership({
contact,
nucleus,
user: leader,
supportStatus: 'engajado',
createdBy: general,
})
const context = await resolveAccessibleNucleusContext(payload, leader, nucleus.slug)
const view = await getNucleusCoordinatorAssignmentPageData(payload, leader, context)
expect(view.coordinators).toEqual([
{ id: general.id, name: general.name, phone: null },
{ id: coordinator.id, name: coordinator.name, phone },
])
expect(JSON.stringify(view)).not.toContain('email')
expect(JSON.stringify(view)).not.toContain('username')
expect(view).not.toHaveProperty('eligibleOptions')
})
})
})