metis-desk/apps/server/src/tickets.integration.ts
2026-10-07 11:41:56 +02:00

176 lines
7.0 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { InventoryResponseSchema } from "@metis/assets/common";
import {
CompanyResponseSchema,
type TicketDTO,
type TicketInput,
TicketListSchema,
TicketLookupsSchema,
TicketResponseSchema,
} from "@metis/companies/common";
import { z } from "zod/v4";
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
export async function verifyTickets(request: Request, companyId: string, otherId: string) {
const path = `/tickets/${companyId}`;
const key = randomUUID();
const lookupsResponse = await request(`${path}/lookups`);
assert.equal(lookupsResponse.status, 200);
const lookups = TicketLookupsSchema.parse(await lookupsResponse.json());
const list = TicketListSchema.parse(await (await request("/tickets")).json());
const person = lookups.people.find((item) => item.active);
const asset = lookups.assets.find((item) => item.active);
const technician = list.technicians.find((item) => item.active);
assert.ok(person && asset && technician);
const values: TicketInput = {
subject: "Ticket de prueba",
description: "Correo no recibe mensajes",
requesterId: person.id,
assigneeId: technician.id,
state: "open",
priority: "normal",
channel: "phone",
affected: [
{ kind: "asset", targetId: asset.id },
{ kind: "person", targetId: person.id },
],
};
const body = { values, requestKey: key, expectedRevisionId: null };
async function error(url: string, payload: unknown, status: number, code: string) {
const response = await request(url, payload);
assert.equal(response.status, status, await response.clone().text());
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
}
const createdResponse = await request(path, body);
assert.equal(createdResponse.status, 201, await createdResponse.clone().text());
const created = TicketResponseSchema.parse(await createdResponse.json()).ticket;
assert.equal(created.id[14], "7");
assert.match(created.reference, /^T-[0-9]{6,}$/);
assert.equal(created.history.length, 1);
assert.equal(created.requesterName, person.name);
assert.equal(created.assigneeName, technician.name);
const repeatedResponse = await request(path, body);
assert.equal(repeatedResponse.status, 201);
assert.equal(TicketResponseSchema.parse(await repeatedResponse.json()).ticket.id, created.id);
await error(path, { ...body, values: { ...values, subject: "Otro" } }, 409, "REQUEST_KEY_REUSED");
await error(`/tickets/${otherId}`, body, 409, "REQUEST_KEY_REUSED");
const otherPerson = TicketLookupsSchema.parse(
await (await request(`/tickets/${otherId}/lookups`)).json()
).people[0];
if (otherPerson) {
await error(
path,
{ ...body, requestKey: randomUUID(), values: { ...values, requesterId: otherPerson.id } },
409,
"REQUESTER_UNAVAILABLE"
);
}
await error(
`/tickets/${otherId}/${created.id}/update`,
{ ...body, expectedRevisionId: created.history[0]?.id },
404,
"TICKET_NOT_FOUND"
);
await error(
path,
{
...body,
requestKey: randomUUID(),
values: { ...values, affected: [{ kind: "resource", targetId: asset.id }] },
},
409,
"TARGET_UNAVAILABLE"
);
await error(
path,
{ ...body, requestKey: randomUUID(), values: { ...values, assigneeId: randomUUID() } },
409,
"ASSIGNEE_UNAVAILABLE"
);
await error(path, { ...body, values: { ...values, subject: "" } }, 400, "INVALID_REQUEST");
assert.equal((await request(path, body, "http://evil.example")).status, 403);
assert.equal((await request(path, body, undefined, "invalid")).status, 403);
assert.equal((await request("/tickets?unknown=1")).status, 400);
async function update(ticket: TicketDTO, next: TicketInput) {
const response = await request(`${path}/${ticket.id}/update`, {
values: next,
requestKey: randomUUID(),
expectedRevisionId: ticket.history.at(-1)?.id,
});
assert.equal(response.status, 200, await response.clone().text());
return TicketResponseSchema.parse(await response.json()).ticket;
}
const same = await update(created, values);
assert.equal(same.history.length, 1);
const updated = await update(same, { ...values, state: "in_progress", priority: "high" });
assert.equal(updated.history.length, 2);
assert.equal(updated.reference, created.reference);
assert.equal(updated.history[0]?.values.state, "open");
await error(
`${path}/${created.id}/update`,
{ ...body, expectedRevisionId: created.history[0]?.id },
409,
"TICKET_CHANGED"
);
const concurrent = await Promise.all(
["resolved", "waiting"].map((state) =>
request(`${path}/${created.id}/update`, {
...body,
requestKey: randomUUID(),
expectedRevisionId: updated.history.at(-1)?.id,
values: { ...updated.values, state },
})
)
);
assert.deepEqual(concurrent.map((response) => response.status).sort(), [200, 409]);
const other = await request(`/tickets/${otherId}`, {
values: { ...values, requesterId: null, assigneeId: null, affected: [] },
requestKey: randomUUID(),
expectedRevisionId: null,
});
assert.equal(other.status, 201);
const otherTicket = TicketResponseSchema.parse(await other.json()).ticket;
assert.notEqual(otherTicket.reference, created.reference);
const global = TicketListSchema.parse(await (await request("/tickets")).json());
assert.ok(global.tickets.some((item) => item.id === created.id));
assert.ok(global.tickets.some((item) => item.id === otherTicket.id));
const scoped = TicketListSchema.parse(await (await request(path)).json());
assert.ok(scoped.tickets.every((item) => item.companyId === companyId));
const originalPerson = CompanyResponseSchema.parse(
await (await request(`/companies/${companyId}`)).json()
).company.people.find((item) => item.id === person.id);
assert.ok(originalPerson);
const personValues = {
name: originalPerson.name,
position: originalPerson.position,
email: originalPerson.email,
phone: originalPerson.phone,
active: originalPerson.active,
isContact: originalPerson.isContact,
};
const disabledPerson = await request(`/companies/${companyId}/people/${person.id}/update`, {
...personValues,
active: false,
});
assert.equal(disabledPerson.status, 200);
CompanyResponseSchema.parse(await disabledPerson.json());
const current = scoped.tickets.find((item) => item.id === created.id);
assert.ok(current);
const retained = await update(current, {
...current.values,
description: "Referencia histórica retenida",
});
assert.equal(retained.values.requesterId, person.id);
await error(path, { ...body, requestKey: randomUUID() }, 409, "REQUESTER_UNAVAILABLE");
assert.equal(
(await request(`/companies/${companyId}/people/${person.id}/update`, personValues)).status,
200
);
const host = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
).assets.find((item) => item.id === asset.id);
assert.ok(host);
}