teqo/tests/unit/contactNameInvariant.unit.spec.ts
Francisco Solla e71b062650 feat(C139): schemas e actions de contato — criação staff e edição por célula
Fase 3 (server) do plano aprovado:
- schemas/contact.ts: contactCreateSchema (nome único via invariante, state
  obrigatório, phones opcionais C112); contactFieldUpdateSchema ganha variantes
  city e postalCode ('' → clear); mensagens das actions client-safe aqui.
- schemas/personGenders.ts: enum de gênero em módulo folha (ciclo de imports
  contact ↔ leadership detectado e quebrado).
- actions/contact.ts: createContact (gate staff + bypass justificado, lock
  contact-name:{normalized}) e updateContactField (escopo 'edita o que vê'
  via re-check user-threaded para advisor; reorder de telefone primário).
  Apagar reusa as actions C100 existentes.
- collections Contact e Supporter: label de gênero 'Outro' → 'Não binário'
  (sem migration; nenhum teste pina a label).
- 11 testes unit novos; lint, tsc e prettier limpos.
2026-08-13 14:12:53 -03:00

66 lines
2.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { Payload } from 'payload'
import { CONTACT_NAME_CONFLICT_MESSAGE } from '@/lib/schemas/contact'
import {
assertContactNameAvailable,
normalizeContactName,
} from '@/utilities/contacts/contactNameInvariant'
describe('normalizeContactName', () => {
it('trims, collapses internal whitespace and lowercases', () => {
expect(normalizeContactName(' Maria da Silva ')).toBe('maria da silva')
expect(normalizeContactName('João')).toBe('joão')
expect(normalizeContactName('')).toBe('')
})
})
describe('assertContactNameAvailable', () => {
type WhereClause = {
and?: { name?: { like?: string }; id?: { not_equals?: number } }[]
}
const makePayload = (docs: { name: string; id?: number }[]): Pick<Payload, 'db' | 'find'> => ({
db: {
name: 'postgres',
sessions: { tx: { db: { execute: vi.fn() } } },
} as unknown as Payload['db'],
find: vi.fn().mockImplementation(async ({ where }: { where?: WhereClause }) => {
const conditions = where?.and ?? []
const idExclusion = conditions.find((c) => c.id?.not_equals)?.id?.not_equals
const like = conditions.find((c) => c.name?.like)?.name?.like
const filtered = docs.filter((doc) => {
if (idExclusion !== undefined && doc.id === idExclusion) return false
if (like && !doc.name.toLowerCase().includes(like)) return false
return true
})
return { docs: filtered, totalDocs: filtered.length }
}) as unknown as Payload['find'],
})
it('passes when no stored contact normalizes to the same name', async () => {
const payload = makePayload([{ id: 9, name: 'Maria Souza' }])
await expect(
assertContactNameAvailable(payload, { transactionID: 'tx' }, ' maria silva '),
).resolves.toBeUndefined()
// The DB prefilter runs the case-insensitive substring, the verdict is in memory.
expect(payload.find).toHaveBeenCalledOnce()
})
it('throws the conflict message on a normalized-name match (case/whitespace insensitive)', async () => {
const payload = makePayload([{ id: 9, name: 'Maria Silva' }])
await expect(assertContactNameAvailable(payload, {}, 'MARIA SILVA')).rejects.toThrow(
CONTACT_NAME_CONFLICT_MESSAGE,
)
})
it('ignores the contact itself when renaming (contactID excluded)', async () => {
const payload = makePayload([{ id: 9, name: 'Maria Silva' }])
await expect(assertContactNameAvailable(payload, {}, 'Maria Silva', 9)).resolves.toBeUndefined()
})
it('does not collide on a partial-name substring match', async () => {
const payload = makePayload([{ id: 9, name: 'Ana Maria' }])
await expect(assertContactNameAvailable(payload, {}, 'Maria')).resolves.toBeUndefined()
})
})