255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
import { describe, expect, it, vi } from "bun:test"
|
|
|
|
import type { Doc, Id } from "../convex/_generated/dataModel"
|
|
import { getTicketsHistoryStatsHandler, listTicketsHistoryHandler } from "../convex/machines"
|
|
|
|
const MACHINE_ID = "machine_1" as Id<"machines">
|
|
const TENANT_ID = "tenant-1"
|
|
|
|
function buildMachine(overrides: Partial<Doc<"machines">> = {}): Doc<"machines"> {
|
|
const machine: Record<string, unknown> = {
|
|
_id: MACHINE_ID,
|
|
tenantId: TENANT_ID,
|
|
hostname: "desktop-01",
|
|
macAddresses: [],
|
|
serialNumbers: [],
|
|
fingerprint: "fp",
|
|
isActive: true,
|
|
lastHeartbeatAt: Date.now(),
|
|
createdAt: Date.now() - 10_000,
|
|
updatedAt: Date.now() - 5_000,
|
|
linkedUserIds: [],
|
|
remoteAccess: null,
|
|
}
|
|
return { ...(machine as Doc<"machines">), ...overrides }
|
|
}
|
|
|
|
function buildTicket(overrides: Partial<Doc<"tickets">> = {}): Doc<"tickets"> {
|
|
const base: Record<string, unknown> = {
|
|
_id: "ticket_base" as Id<"tickets">,
|
|
tenantId: TENANT_ID,
|
|
reference: 42600,
|
|
subject: "Generic ticket",
|
|
summary: "",
|
|
status: "PENDING",
|
|
priority: "MEDIUM",
|
|
channel: "EMAIL",
|
|
queueId: undefined,
|
|
requesterId: "user_req" as Id<"users">,
|
|
requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] },
|
|
assigneeId: "user_assignee" as Id<"users">,
|
|
assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] },
|
|
companyId: undefined,
|
|
companySnapshot: undefined,
|
|
machineId: MACHINE_ID,
|
|
machineSnapshot: undefined,
|
|
working: false,
|
|
dueAt: undefined,
|
|
firstResponseAt: undefined,
|
|
resolvedAt: undefined,
|
|
closedAt: undefined,
|
|
updatedAt: Date.now(),
|
|
createdAt: Date.now() - 2000,
|
|
tags: [],
|
|
customFields: [],
|
|
totalWorkedMs: 0,
|
|
internalWorkedMs: 0,
|
|
externalWorkedMs: 0,
|
|
activeSessionId: undefined,
|
|
}
|
|
return { ...(base as Doc<"tickets">), ...overrides }
|
|
}
|
|
|
|
type PaginateHandler = (options: { cursor: string | null; numItems: number }) => Promise<{
|
|
page: Doc<"tickets">[]
|
|
isDone: boolean
|
|
continueCursor: string
|
|
}>
|
|
|
|
function createCtx({
|
|
machine = buildMachine(),
|
|
queues = new Map<string, Doc<"queues">>(),
|
|
paginate,
|
|
}: {
|
|
machine?: Doc<"machines">
|
|
queues?: Map<string, Doc<"queues">>
|
|
paginate: PaginateHandler
|
|
}) {
|
|
const createFilterBuilder = () => {
|
|
const builder: Record<string, (..._args: unknown[]) => typeof builder> = {}
|
|
builder.eq = () => builder
|
|
builder.gte = () => builder
|
|
builder.lte = () => builder
|
|
builder.or = () => builder
|
|
return builder
|
|
}
|
|
|
|
return {
|
|
db: {
|
|
get: vi.fn(async (id: Id<"machines"> | Id<"queues">) => {
|
|
if (machine && id === machine._id) return machine
|
|
const queue = queues.get(String(id))
|
|
if (queue) return queue
|
|
return null
|
|
}),
|
|
query: vi.fn(() => {
|
|
const chain = {
|
|
filter: vi.fn((cb?: (builder: ReturnType<typeof createFilterBuilder>) => unknown) => {
|
|
cb?.(createFilterBuilder())
|
|
return chain
|
|
}),
|
|
paginate: vi.fn((options: { cursor: string | null; numItems: number }) => paginate(options)),
|
|
}
|
|
|
|
return {
|
|
withIndex: vi.fn((_indexName: string, cb?: (builder: ReturnType<typeof createFilterBuilder>) => unknown) => {
|
|
cb?.(createFilterBuilder())
|
|
return {
|
|
order: vi.fn(() => chain),
|
|
}
|
|
}),
|
|
}
|
|
}),
|
|
},
|
|
} as unknown as Parameters<typeof listTicketsHistoryHandler>[0]
|
|
}
|
|
|
|
describe("convex.machines.listTicketsHistory", () => {
|
|
it("maps tickets metadata and resolves queue names", async () => {
|
|
const machine = buildMachine()
|
|
const ticket = buildTicket({
|
|
_id: "ticket_1" as Id<"tickets">,
|
|
subject: "Printer offline",
|
|
priority: "HIGH",
|
|
status: "PENDING",
|
|
queueId: "queue_1" as Id<"queues">,
|
|
updatedAt: 170000,
|
|
createdAt: 160000,
|
|
})
|
|
|
|
const paginate = vi.fn(async () => ({
|
|
page: [ticket],
|
|
isDone: false,
|
|
continueCursor: "cursor-next",
|
|
}))
|
|
|
|
const queues = new Map<string, Doc<"queues">>([
|
|
["queue_1", { _id: "queue_1" as Id<"queues">, name: "Atendimento", tenantId: TENANT_ID } as Doc<"queues">],
|
|
])
|
|
|
|
const ctx = createCtx({ machine, queues, paginate })
|
|
|
|
const result = await listTicketsHistoryHandler(ctx, {
|
|
machineId: machine._id,
|
|
paginationOpts: { numItems: 25, cursor: null },
|
|
})
|
|
|
|
expect(paginate).toHaveBeenCalledWith({ numItems: 25, cursor: null })
|
|
expect(result.page).toHaveLength(1)
|
|
expect(result.page[0]).toMatchObject({
|
|
id: "ticket_1",
|
|
subject: "Printer offline",
|
|
priority: "HIGH",
|
|
status: "PENDING",
|
|
queue: "Atendimento",
|
|
})
|
|
expect(result.continueCursor).toBe("cursor-next")
|
|
})
|
|
|
|
it("applies search filtering over paginated results", async () => {
|
|
const machine = buildMachine()
|
|
const ticketMatches = buildTicket({
|
|
_id: "ticket_match" as Id<"tickets">,
|
|
reference: 44321,
|
|
subject: "Notebook com tela quebrada",
|
|
requesterSnapshot: { name: "Carla", email: "carla@example.com", avatarUrl: undefined, teams: [] },
|
|
})
|
|
const ticketIgnored = buildTicket({
|
|
_id: "ticket_other" as Id<"tickets">,
|
|
subject: "Troca de teclado",
|
|
requesterSnapshot: { name: "Roberto", email: "roberto@example.com", avatarUrl: undefined, teams: [] },
|
|
})
|
|
|
|
const paginate = vi.fn(async () => ({
|
|
page: [ticketMatches, ticketIgnored],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
}))
|
|
|
|
const ctx = createCtx({ machine, paginate })
|
|
|
|
const result = await listTicketsHistoryHandler(ctx, {
|
|
machineId: machine._id,
|
|
search: "notebook",
|
|
paginationOpts: { numItems: 50, cursor: null },
|
|
})
|
|
|
|
expect(result.page).toHaveLength(1)
|
|
expect(result.page[0].id).toBe("ticket_match")
|
|
expect(result.isDone).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("convex.machines.getTicketsHistoryStats", () => {
|
|
it("aggregates totals across multiple pages respecting open status", async () => {
|
|
const machine = buildMachine()
|
|
const firstPageTicket = buildTicket({
|
|
_id: "ticket_open" as Id<"tickets">,
|
|
status: "AWAITING_ATTENDANCE",
|
|
})
|
|
const secondPageTicket = buildTicket({
|
|
_id: "ticket_resolved" as Id<"tickets">,
|
|
status: "RESOLVED",
|
|
})
|
|
|
|
const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => {
|
|
if (!cursor) {
|
|
return { page: [firstPageTicket], isDone: false, continueCursor: "cursor-1" }
|
|
}
|
|
return { page: [secondPageTicket], isDone: true, continueCursor: "" }
|
|
})
|
|
|
|
const ctx = createCtx({ machine, paginate })
|
|
|
|
const stats = await getTicketsHistoryStatsHandler(
|
|
ctx as unknown as Parameters<typeof getTicketsHistoryStatsHandler>[0],
|
|
{
|
|
machineId: machine._id,
|
|
}
|
|
)
|
|
|
|
expect(stats).toEqual({ total: 2, openCount: 1, resolvedCount: 1 })
|
|
expect(paginate).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it("filters results by search term when aggregating", async () => {
|
|
const machine = buildMachine()
|
|
const matchingTicket = buildTicket({
|
|
_id: "ticket_search" as Id<"tickets">,
|
|
subject: "Notebook com lentidão",
|
|
})
|
|
const nonMatchingTicket = buildTicket({
|
|
_id: "ticket_ignored" as Id<"tickets">,
|
|
subject: "Impressora parada",
|
|
})
|
|
|
|
const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => {
|
|
if (!cursor) {
|
|
return { page: [matchingTicket, nonMatchingTicket], isDone: true, continueCursor: "" }
|
|
}
|
|
return { page: [], isDone: true, continueCursor: "" }
|
|
})
|
|
|
|
const ctx = createCtx({ machine, paginate })
|
|
|
|
const stats = await getTicketsHistoryStatsHandler(
|
|
ctx as unknown as Parameters<typeof getTicketsHistoryStatsHandler>[0],
|
|
{
|
|
machineId: machine._id,
|
|
search: "notebook",
|
|
}
|
|
)
|
|
|
|
expect(stats).toEqual({ total: 1, openCount: 1, resolvedCount: 0 })
|
|
})
|
|
})
|