sistema-de-chamados/tests/tickets.submitCsat.test.ts

219 lines
6.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "bun:test"
import type { Doc, Id } from "../convex/_generated/dataModel"
import { submitCsatHandler } from "../convex/tickets"
import { requireUser } from "../convex/rbac"
vi.mock("../convex/rbac", () => ({
requireUser: vi.fn(),
requireStaff: vi.fn(),
requireAdmin: vi.fn(),
}))
type SubmitCsatCtx = Parameters<typeof submitCsatHandler>[0]
const mockedRequireUser = vi.mocked(requireUser)
const FIXED_NOW = 1_706_500_000_000
function makeTicket(overrides: Partial<Doc<"tickets">> = {}): Doc<"tickets"> {
const ticket = {
_id: "ticket_1" as Id<"tickets">,
_creationTime: FIXED_NOW - 10_000,
tenantId: "tenant-1",
reference: 42_100,
subject: "Computador não liga",
summary: null,
status: "RESOLVED",
priority: "MEDIUM",
channel: "EMAIL",
queueId: null,
queueSnapshot: null,
requesterId: "user_requester" as Id<"users">,
requesterSnapshot: { name: "Cliente", email: "cliente@example.com" },
assigneeId: null,
assigneeSnapshot: null,
companyId: null,
companySnapshot: null,
machineId: null,
machineSnapshot: null,
slaPolicyId: null,
dueAt: null,
firstResponseAt: null,
resolvedAt: FIXED_NOW - 1000,
createdAt: FIXED_NOW - 20_000,
updatedAt: FIXED_NOW - 100,
tags: [],
customFields: [],
activeSessionId: null,
totalWorkedMs: 0,
internalWorkedMs: 0,
externalWorkedMs: 0,
csatScore: undefined,
csatMaxScore: undefined,
csatComment: undefined,
csatRatedAt: undefined,
csatRatedBy: undefined,
} satisfies Partial<Doc<"tickets">>
return { ...(ticket as Doc<"tickets">), ...overrides }
}
function createCtx(ticket: Doc<"tickets">, events: Doc<"ticketEvents">[] = []) {
const collect = vi.fn(async () => events)
const query = vi.fn(() => ({
withIndex: vi.fn((_index: string, cb: (builder: { eq: (field: string, value: unknown) => { collect: typeof collect } }) => { collect: typeof collect }) => {
const cursor = {
eq: vi.fn(() => ({
collect,
})),
}
const result = cb(cursor as { eq: (field: string, value: unknown) => { collect: typeof collect } })
return result ?? { collect }
}),
}))
const patch = vi.fn(async () => undefined)
const insert = vi.fn(async () => undefined)
const deleteFn = vi.fn(async () => undefined)
const ctx: SubmitCsatCtx = {
db: {
get: vi.fn(async (id: Id<"tickets"> | Id<"users">) => {
if (id === ticket._id) return ticket
return null
}),
patch,
query,
delete: deleteFn,
insert,
},
} as unknown as SubmitCsatCtx
return { ctx, patch, insert, deleteFn, query }
}
describe("convex.tickets.submitCsat", () => {
afterEach(() => {
vi.clearAllMocks()
})
it("permite que o solicitante avalie um chamado resolvido", async () => {
const ticket = makeTicket()
const { ctx, patch, insert, deleteFn } = createCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: "user_requester" as Id<"users">,
email: "cliente@example.com",
companyId: null,
},
role: "COLLABORATOR",
})
const result = await submitCsatHandler(ctx, {
ticketId: ticket._id,
actorId: "user_requester" as Id<"users">,
score: 4,
maxScore: 5,
comment: "Atendimento excelente!",
})
expect(result.score).toBe(4)
expect(result.comment).toBe("Atendimento excelente!")
expect(patch).toHaveBeenCalledWith(
ticket._id,
expect.objectContaining({
csatScore: 4,
csatMaxScore: 5,
csatComment: "Atendimento excelente!",
csatRatedBy: "user_requester",
})
)
expect(insert).toHaveBeenCalledWith(
"ticketEvents",
expect.objectContaining({
ticketId: ticket._id,
type: "CSAT_RATED",
payload: expect.objectContaining({ score: 4, maxScore: 5, comment: "Atendimento excelente!" }),
})
)
expect(deleteFn).not.toHaveBeenCalled()
})
it("bloqueia avaliações antes do encerramento do chamado para colaboradores", async () => {
const ticket = makeTicket({ status: "PENDING" })
const { ctx, patch, insert } = createCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: "user_requester" as Id<"users">,
email: "cliente@example.com",
companyId: null,
},
role: "COLLABORATOR",
})
await expect(
submitCsatHandler(ctx, {
ticketId: ticket._id,
actorId: "user_requester" as Id<"users">,
score: 5,
comment: "Perfeito",
})
).rejects.toThrow("Avaliações só são permitidas após o encerramento do chamado")
expect(patch).not.toHaveBeenCalled()
expect(insert).not.toHaveBeenCalled()
})
it("não permite registrar uma segunda avaliação", async () => {
const ticket = makeTicket({
csatScore: 4,
csatMaxScore: 5,
csatRatedAt: FIXED_NOW - 2000,
csatRatedBy: "user_requester" as Id<"users">,
})
const { ctx, patch, insert, deleteFn } = createCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: "user_requester" as Id<"users">,
email: "cliente@example.com",
companyId: null,
},
role: "COLLABORATOR",
})
await expect(
submitCsatHandler(ctx, {
ticketId: ticket._id,
actorId: "user_requester" as Id<"users">,
score: 5,
comment: "Vou tentar atualizar",
})
).rejects.toThrow("Este chamado já possui uma avaliação registrada")
expect(patch).not.toHaveBeenCalled()
expect(insert).not.toHaveBeenCalled()
expect(deleteFn).not.toHaveBeenCalled()
})
it("impede que administradores registrem avaliação", async () => {
const ticket = makeTicket()
const { ctx, patch, insert } = createCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: "user_admin" as Id<"users">,
email: "admin@example.com",
companyId: null,
},
role: "ADMIN",
})
await expect(
submitCsatHandler(ctx, {
ticketId: ticket._id,
actorId: "user_admin" as Id<"users">,
score: 4,
comment: "somente testes",
})
).rejects.toThrow("Somente o solicitante pode avaliar o chamado")
expect(patch).not.toHaveBeenCalled()
expect(insert).not.toHaveBeenCalled()
})
})