teqo-backup/tests/unit/campaignMunicipalityAdvisorsRoute.unit.spec.ts
Francisco Ignácio Fontoura Solla 26b2205c3a B154 — criar assessor inline no popover da lista de municípios
Closes #352

- POST /campanha/municipios/advisors aceita body de criação (name) na union;
  createMunicipalityAdvisorRecord cria campaignUser advisor (stub
  <slug>@criado.invalid, senha aleatória) e atribui ao município na mesma
  transação (cap 10 com rollback, lock municipality-advisors:{id})
- MunicipalityAdvisorCreateProvider compartilha o assessor criado entre linhas;
  opção "+ Criar assessor" e chip otimista com id temporário no control
- advisorNameSchema unificado em primitives.ts; predicado de placeholder cobre
  @criado.invalid (reset de senha e célula de e-mail tratam stubs inline)
- Destrava o spec e2e de municipios (falhas pré-existentes em main): key
  warning do fragment trailing de MunicipalityFilters; settle-wait da animação
  do Drawer no teste B136
2026-08-03 22:46:06 -03:00

88 lines
3.4 KiB
TypeScript

// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { POST } from '@/app/(campaign)/campanha/(app)/municipios/advisors/route'
import type { MunicipalityListAdvisorsResponse } from '@/app/(campaign)/campanha/(app)/municipios/advisors/types'
import {
createMunicipalityAdvisor,
setMunicipalityAdvisorMembership,
} from '@/app/(campaign)/campanha/actions/municipality'
vi.mock('@/app/(campaign)/campanha/actions/municipality', () => ({
createMunicipalityAdvisor: vi.fn(),
setMunicipalityAdvisorMembership: vi.fn(),
}))
const mockedCreate = vi.mocked(createMunicipalityAdvisor)
const mockedToggle = vi.mocked(setMunicipalityAdvisorMembership)
const postRequest = (body: unknown): Request =>
new Request('http://localhost/campanha/municipios/advisors', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Origin: 'http://localhost' },
body: JSON.stringify(body),
})
const jsonBody = async (response: Response): Promise<MunicipalityListAdvisorsResponse> =>
(await response.json()) as MunicipalityListAdvisorsResponse
/**
* B154 — the endpoint contract is a discriminated union: the B27 toggle
* (`advisorId` + `assigned`, no `name`) and the name-only create (`name`, no
* `advisorId`) dispatch to different actions. These cases pin that a create
* body can never silently fall through to the toggle (or vice-versa).
*/
describe('POST /campanha/municipios/advisors body union (B154)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('dispatches the B27 toggle body to setMunicipalityAdvisorMembership', async () => {
mockedToggle.mockResolvedValue({
advisors: [{ id: 7 }],
} as Awaited<ReturnType<typeof setMunicipalityAdvisorMembership>>)
const response = await POST(postRequest({ municipalityId: 1, advisorId: 7, assigned: true }))
expect(mockedCreate).not.toHaveBeenCalled()
expect(mockedToggle).toHaveBeenCalledWith({ municipality: 1, advisor: 7, assigned: true })
expect(response.status).toBe(200)
await expect(jsonBody(response)).resolves.toMatchObject({
status: 'success',
advisors: [7],
})
})
it('dispatches the name-only create body to createMunicipalityAdvisor', async () => {
mockedCreate.mockResolvedValue({
advisors: [{ id: 9 }],
createdAdvisorId: 9,
} as Awaited<ReturnType<typeof createMunicipalityAdvisor>>)
const response = await POST(postRequest({ municipalityId: 1, name: 'Carlos' }))
expect(mockedToggle).not.toHaveBeenCalled()
expect(mockedCreate).toHaveBeenCalledWith({ municipality: 1, name: 'Carlos' })
expect(response.status).toBe(200)
await expect(jsonBody(response)).resolves.toMatchObject({
status: 'success',
advisors: [9],
createdAdvisor: { id: 9, name: 'Carlos' },
})
})
it.each([
['no dispatch shape at all', { municipalityId: 1 }],
['both shapes at once', { municipalityId: 1, advisorId: 7, assigned: true, name: 'Carlos' }],
['create with a toggle remnant', { municipalityId: 1, name: 'Carlos', assigned: false }],
['name too short', { municipalityId: 1, name: 'C' }],
['blank name', { municipalityId: 1, name: ' ' }],
])('rejects %s with 400 without calling either action', async (_label, body) => {
const response = await POST(postRequest(body))
expect(mockedToggle).not.toHaveBeenCalled()
expect(mockedCreate).not.toHaveBeenCalled()
expect(response.status).toBe(400)
})
})