mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
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
88 lines
3.4 KiB
TypeScript
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)
|
|
})
|
|
})
|