238 lines
6.9 KiB
TypeScript
238 lines
6.9 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: undefined,
|
|
status: "RESOLVED",
|
|
priority: "MEDIUM",
|
|
channel: "EMAIL",
|
|
queueId: undefined,
|
|
requesterId: "user_requester" as Id<"users">,
|
|
requesterSnapshot: { name: "Cliente", email: "cliente@example.com" },
|
|
assigneeId: undefined,
|
|
assigneeSnapshot: undefined,
|
|
companyId: undefined,
|
|
companySnapshot: undefined,
|
|
machineId: undefined,
|
|
machineSnapshot: undefined,
|
|
slaPolicyId: undefined,
|
|
dueAt: undefined,
|
|
firstResponseAt: undefined,
|
|
resolvedAt: FIXED_NOW - 1000,
|
|
createdAt: FIXED_NOW - 20_000,
|
|
updatedAt: FIXED_NOW - 100,
|
|
tags: [],
|
|
customFields: [],
|
|
activeSessionId: undefined,
|
|
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">,
|
|
_creationTime: FIXED_NOW - 30_000,
|
|
tenantId: "tenant-1",
|
|
name: "Cliente",
|
|
email: "cliente@example.com",
|
|
role: "COLLABORATOR",
|
|
companyId: undefined,
|
|
teams: [],
|
|
},
|
|
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">,
|
|
_creationTime: FIXED_NOW - 30_000,
|
|
tenantId: "tenant-1",
|
|
name: "Cliente",
|
|
email: "cliente@example.com",
|
|
role: "COLLABORATOR",
|
|
companyId: undefined,
|
|
teams: [],
|
|
},
|
|
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">,
|
|
_creationTime: FIXED_NOW - 30_000,
|
|
tenantId: "tenant-1",
|
|
name: "Cliente",
|
|
email: "cliente@example.com",
|
|
role: "COLLABORATOR",
|
|
companyId: undefined,
|
|
teams: [],
|
|
},
|
|
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">,
|
|
_creationTime: FIXED_NOW - 40_000,
|
|
tenantId: "tenant-1",
|
|
name: "Administrador",
|
|
email: "admin@example.com",
|
|
role: "ADMIN",
|
|
companyId: undefined,
|
|
teams: [],
|
|
},
|
|
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()
|
|
})
|
|
})
|