sistema-de-chamados/tests/tickets.lifecycle.test.ts
2025-11-08 00:28:52 -03:00

273 lines
7.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"
import type { Doc, Id } from "../convex/_generated/dataModel"
import { resolveTicketHandler, reopenTicketHandler } from "../convex/tickets"
import { requireStaff, requireUser } from "../convex/rbac"
type ResolveCtx = Parameters<typeof resolveTicketHandler>[0]
type ReopenCtx = Parameters<typeof reopenTicketHandler>[0]
type TicketDoc = Doc<"tickets">
type MockedTicket = TicketDoc & { _id: Id<"tickets"> }
defineMocks()
function defineMocks() {
vi.mock("../convex/rbac", () => ({
requireStaff: vi.fn(),
requireUser: vi.fn(),
requireAdmin: vi.fn(),
}))
}
const mockedRequireStaff = vi.mocked(requireStaff)
const mockedRequireUser = vi.mocked(requireUser)
const NOW = 1_706_700_000_000
beforeEach(() => {
vi.setSystemTime(NOW)
})
afterEach(() => {
vi.clearAllMocks()
})
function buildTicket(overrides: Partial<TicketDoc> = {}): MockedTicket {
const base: TicketDoc = {
_id: "ticket_main" as Id<"tickets">,
_creationTime: NOW - 10_000,
tenantId: "tenant-1",
reference: 41_000,
subject: "Computador não liga",
summary: undefined,
status: "AWAITING_ATTENDANCE",
priority: "MEDIUM",
channel: "EMAIL",
queueId: undefined,
requesterId: "user_requester" as Id<"users">,
requesterSnapshot: { name: "Cliente", email: "cliente@example.com" },
assigneeId: "user_agent" as Id<"users">,
assigneeSnapshot: { name: "Agente", email: "agente@example.com" },
companyId: undefined,
companySnapshot: undefined,
machineId: undefined,
machineSnapshot: undefined,
slaPolicyId: undefined,
dueAt: undefined,
firstResponseAt: undefined,
resolvedAt: undefined,
createdAt: NOW - 50_000,
updatedAt: NOW - 1_000,
tags: [],
customFields: [],
activeSessionId: undefined,
totalWorkedMs: 0,
internalWorkedMs: 0,
externalWorkedMs: 0,
csatScore: undefined,
csatMaxScore: undefined,
csatComment: undefined,
csatRatedAt: undefined,
csatRatedBy: undefined,
csatAssigneeId: undefined,
csatAssigneeSnapshot: undefined,
reopenDeadline: undefined,
reopenedAt: undefined,
formTemplate: undefined,
chatEnabled: true,
relatedTicketIds: undefined,
resolvedWithTicketId: undefined,
}
return { ...(base as TicketDoc), ...overrides }
}
function createResolveCtx(tickets: Record<string, TicketDoc>, events: Doc<"ticketEvents">[] = []) {
const patch = vi.fn(async () => undefined)
const insert = vi.fn(async () => undefined)
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 ctx: ResolveCtx = {
db: {
get: vi.fn(async (id: Id<"tickets"> | Id<"users">) => {
return tickets[String(id)] ?? null
}),
patch,
insert,
query,
delete: vi.fn(),
},
} as unknown as ResolveCtx
return { ctx, patch, insert }
}
function createReopenCtx(ticket: TicketDoc) {
const patch = vi.fn(async () => undefined)
const insert = vi.fn(async () => undefined)
const ctx: ReopenCtx = {
db: {
get: vi.fn(async () => ticket),
patch,
insert,
query: vi.fn(),
delete: vi.fn(),
},
} as unknown as ReopenCtx
return { ctx, patch, insert }
}
describe("convex.tickets.resolveTicketHandler", () => {
it("marca o ticket como resolvido, vincula outro ticket e define prazo de reabertura", async () => {
const ticketMain = buildTicket()
const ticketLinked = buildTicket({
_id: "ticket_related" as Id<"tickets">,
reference: 41_001,
status: "AWAITING_ATTENDANCE",
})
const { ctx, patch, insert } = createResolveCtx({
[String(ticketMain._id)]: ticketMain,
[String(ticketLinked._id)]: ticketLinked,
})
mockedRequireStaff.mockResolvedValue({
user: {
_id: "user_agent" as Id<"users">,
_creationTime: NOW - 5_000,
tenantId: "tenant-1",
name: "Agente",
email: "agente@example.com",
role: "ADMIN",
teams: [],
},
role: "ADMIN",
})
const result = await resolveTicketHandler(ctx, {
ticketId: ticketMain._id,
actorId: "user_agent" as Id<"users">,
resolvedWithTicketId: ticketLinked._id,
reopenWindowDays: 14,
})
expect(result.ok).toBe(true)
expect(result.reopenWindowDays).toBe(14)
expect(result.reopenDeadline).toBe(NOW + 14 * 24 * 60 * 60 * 1000)
expect(patch).toHaveBeenCalledWith(
ticketMain._id,
expect.objectContaining({
status: "RESOLVED",
reopenDeadline: result.reopenDeadline,
resolvedWithTicketId: ticketLinked._id,
})
)
expect(patch).toHaveBeenCalledWith(
ticketLinked._id,
expect.objectContaining({
relatedTicketIds: expect.arrayContaining([ticketMain._id]),
})
)
expect(insert).toHaveBeenCalledWith(
"ticketEvents",
expect.objectContaining({
ticketId: ticketMain._id,
type: "STATUS_CHANGED",
payload: expect.objectContaining({ to: "RESOLVED" }),
})
)
expect(insert).toHaveBeenCalledWith(
"ticketEvents",
expect.objectContaining({
ticketId: ticketLinked._id,
type: "TICKET_LINKED",
payload: expect.objectContaining({ linkedTicketId: ticketMain._id }),
})
)
})
})
describe("convex.tickets.reopenTicketHandler", () => {
it("reabre o ticket quando dentro do prazo e permissões válidas", async () => {
const ticket = buildTicket({
status: "RESOLVED",
reopenDeadline: NOW + 3 * 24 * 60 * 60 * 1000,
resolvedAt: NOW - 60_000,
})
const { ctx, patch, insert } = createReopenCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: ticket.requesterId,
_creationTime: NOW - 20_000,
tenantId: "tenant-1",
email: "cliente@example.com",
name: "Cliente",
role: "COLLABORATOR",
companyId: undefined,
teams: [],
},
role: "COLLABORATOR",
})
const result = await reopenTicketHandler(ctx, {
ticketId: ticket._id,
actorId: ticket.requesterId,
})
expect(result.ok).toBe(true)
expect(result.reopenedAt).toBe(NOW)
expect(patch).toHaveBeenCalledWith(
ticket._id,
expect.objectContaining({ status: "AWAITING_ATTENDANCE", reopenedAt: NOW, resolvedAt: undefined })
)
expect(insert).toHaveBeenCalledWith(
"ticketEvents",
expect.objectContaining({ ticketId: ticket._id, type: "TICKET_REOPENED" })
)
})
it("falha quando o prazo para reabrir expirou", async () => {
const ticket = buildTicket({
status: "RESOLVED",
reopenDeadline: NOW - 1,
})
const { ctx } = createReopenCtx(ticket)
mockedRequireUser.mockResolvedValue({
user: {
_id: ticket.requesterId,
_creationTime: NOW - 25_000,
tenantId: "tenant-1",
email: "cliente@example.com",
name: "Cliente",
role: "COLLABORATOR",
companyId: undefined,
teams: [],
},
role: "COLLABORATOR",
})
await expect(
reopenTicketHandler(ctx, {
ticketId: ticket._id,
actorId: ticket.requesterId,
})
).rejects.toThrow("O prazo para reabrir este chamado expirou")
})
})