210 lines
7.3 KiB
TypeScript
210 lines
7.3 KiB
TypeScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import { randomUUID } from "node:crypto";
|
||
|
|
|
||
|
|
import {
|
||
|
|
AssetResponseSchema,
|
||
|
|
InventoryResponseSchema,
|
||
|
|
WorkstationResponseSchema,
|
||
|
|
} from "@metis/assets/common";
|
||
|
|
import {
|
||
|
|
CompanyResponseSchema,
|
||
|
|
TicketListSchema,
|
||
|
|
TicketResponseSchema,
|
||
|
|
} from "@metis/companies/common";
|
||
|
|
import type { Sequelize } from "sequelize";
|
||
|
|
import { QueryTypes } from "sequelize";
|
||
|
|
import { z } from "zod/v4";
|
||
|
|
|
||
|
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
||
|
|
|
||
|
|
export async function verifyWorkstationLifecycle(
|
||
|
|
request: Request,
|
||
|
|
companyId: string,
|
||
|
|
otherId: string,
|
||
|
|
database: Sequelize
|
||
|
|
) {
|
||
|
|
const path = `/assets/${companyId}/workstations`;
|
||
|
|
const inventory = InventoryResponseSchema.parse(
|
||
|
|
await (await request(`/assets/${companyId}`)).json()
|
||
|
|
);
|
||
|
|
const source = inventory.assets[0];
|
||
|
|
|
||
|
|
assert.ok(source);
|
||
|
|
const assets = [];
|
||
|
|
|
||
|
|
for (const name of ["Equipo de baja", "Impresora compartida de baja"]) {
|
||
|
|
const response = await request(`/assets/${companyId}`, {
|
||
|
|
values: { ...source.values, name, status: "available" },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201, await response.clone().text());
|
||
|
|
assets.push(AssetResponseSchema.parse(await response.json()).asset);
|
||
|
|
}
|
||
|
|
const [computer, shared] = assets;
|
||
|
|
|
||
|
|
assert.ok(computer && shared);
|
||
|
|
const personResponse = await request(`/companies/${companyId}/people`, {
|
||
|
|
name: "Responsable baja",
|
||
|
|
position: null,
|
||
|
|
email: null,
|
||
|
|
phone: null,
|
||
|
|
active: true,
|
||
|
|
isContact: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(personResponse.status, 201);
|
||
|
|
const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1);
|
||
|
|
|
||
|
|
assert.ok(person);
|
||
|
|
const values = {
|
||
|
|
name: "Puesto para baja",
|
||
|
|
active: true,
|
||
|
|
siteId: source.values.siteId,
|
||
|
|
personId: person.id,
|
||
|
|
computerAssetId: computer.id,
|
||
|
|
assetIds: [computer.id],
|
||
|
|
sharedAssetIds: [shared.id],
|
||
|
|
};
|
||
|
|
|
||
|
|
const create = async (data = values) => {
|
||
|
|
const response = await request(path, { values: data });
|
||
|
|
|
||
|
|
assert.equal(response.status, 201, await response.clone().text());
|
||
|
|
|
||
|
|
return WorkstationResponseSchema.parse(await response.json()).workstation;
|
||
|
|
};
|
||
|
|
|
||
|
|
const station = await create();
|
||
|
|
const retirePath = `${path}/${station.id}/decommission`;
|
||
|
|
const deletePath = `${path}/${station.id}/delete`;
|
||
|
|
|
||
|
|
for (const action of ["decommission", "delete"]) {
|
||
|
|
assert.equal(
|
||
|
|
(await request(`/assets/${otherId}/workstations/${station.id}/${action}`, {})).status,
|
||
|
|
404
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await request(`${path}/${station.id}/${action}`, {}, "https://foreign.example")).status,
|
||
|
|
403
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await request(`${path}/${station.id}/${action}`, {}, undefined, "a".repeat(64))).status,
|
||
|
|
403
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await request(`${path}/${station.id}/${action}`, { actorId: person.id })).status,
|
||
|
|
400
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const response = await request(retirePath, {});
|
||
|
|
|
||
|
|
assert.equal(response.status, 200);
|
||
|
|
const retired = WorkstationResponseSchema.parse(await response.json()).workstation;
|
||
|
|
|
||
|
|
assert.equal(retired.values.active, false);
|
||
|
|
assert.equal(retired.values.personId, null);
|
||
|
|
assert.equal(retired.values.computerAssetId ?? null, null);
|
||
|
|
assert.deepEqual(retired.values.assetIds, []);
|
||
|
|
assert.deepEqual(retired.values.sharedAssetIds, []);
|
||
|
|
assert.equal(retired.history.length, 2);
|
||
|
|
assert.deepEqual(retired.history[0]?.values.assetIds, [computer.id]);
|
||
|
|
assert.deepEqual(
|
||
|
|
WorkstationResponseSchema.parse(await (await request(retirePath, {})).json()).workstation,
|
||
|
|
retired
|
||
|
|
);
|
||
|
|
// Ambas clases de composición pueden reservarse de nuevo tras la baja.
|
||
|
|
const replacement = await create({
|
||
|
|
...values,
|
||
|
|
name: "Puesto reutiliza activos",
|
||
|
|
assetIds: [computer.id, shared.id],
|
||
|
|
sharedAssetIds: [],
|
||
|
|
});
|
||
|
|
const protectedResponse = await request(`/tickets/${companyId}`, {
|
||
|
|
values: {
|
||
|
|
subject: "Conservar referencia a puesto",
|
||
|
|
description: "",
|
||
|
|
requesterId: null,
|
||
|
|
assigneeId: null,
|
||
|
|
state: "open",
|
||
|
|
priority: "normal",
|
||
|
|
channel: "internal",
|
||
|
|
affected: [{ kind: "workstation", targetId: replacement.id }],
|
||
|
|
},
|
||
|
|
requestKey: randomUUID(),
|
||
|
|
expectedRevisionId: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(protectedResponse.status, 201, await protectedResponse.clone().text());
|
||
|
|
const ticket = TicketResponseSchema.parse(await protectedResponse.json()).ticket;
|
||
|
|
const blocked = await request(`${path}/${replacement.id}/delete`, {});
|
||
|
|
|
||
|
|
assert.equal(blocked.status, 409);
|
||
|
|
assert.equal(
|
||
|
|
z.object({ code: z.string() }).parse(await blocked.json()).code,
|
||
|
|
"WORKSTATION_IN_USE"
|
||
|
|
);
|
||
|
|
const retained = InventoryResponseSchema.parse(
|
||
|
|
await (await request(`/assets/${companyId}`)).json()
|
||
|
|
).workstations.find((item) => item.id === replacement.id);
|
||
|
|
|
||
|
|
assert.deepEqual(retained, replacement);
|
||
|
|
assert.equal((await request(`${path}/${replacement.id}/decommission`, {})).status, 200);
|
||
|
|
const retainedTicket = TicketListSchema.parse(
|
||
|
|
await (await request(`/tickets/${companyId}`)).json()
|
||
|
|
).tickets.find((item) => item.id === ticket.id);
|
||
|
|
|
||
|
|
assert.ok(retainedTicket);
|
||
|
|
assert.deepEqual(retainedTicket.values.affected, ticket.values.affected);
|
||
|
|
assert.equal((await request(deletePath, {})).status, 200);
|
||
|
|
assert.equal((await request(deletePath, {})).status, 404);
|
||
|
|
const after = InventoryResponseSchema.parse(await (await request(`/assets/${companyId}`)).json());
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
after.workstations.some((item) => item.id === station.id),
|
||
|
|
false
|
||
|
|
);
|
||
|
|
assert.ok(after.assets.some((item) => item.id === computer.id));
|
||
|
|
assert.ok(after.assets.some((item) => item.id === shared.id));
|
||
|
|
const rows = await database.query<{ total: number }>(
|
||
|
|
"SELECT COUNT(*) AS total FROM workstation_revisions WHERE workstationId=:id",
|
||
|
|
{ replacements: { id: station.id }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(Number(rows[0]?.total), 0);
|
||
|
|
for (const table of ["workstation_assets", "workstation_members"]) {
|
||
|
|
const remaining = await database.query<{ total: number }>(
|
||
|
|
`SELECT COUNT(*) AS total FROM ${table} WHERE workstationId=:id`,
|
||
|
|
{ replacements: { id: station.id }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(Number(remaining[0]?.total), 0);
|
||
|
|
}
|
||
|
|
// La eliminación directa de un puesto activo libera su reserva sin borrar activos.
|
||
|
|
const direct = await create({ ...values, name: "Eliminar puesto activo", sharedAssetIds: [] });
|
||
|
|
|
||
|
|
assert.equal((await request(`${path}/${direct.id}/delete`, {})).status, 200);
|
||
|
|
const reusable = await create({
|
||
|
|
...values,
|
||
|
|
name: "Reserva después de eliminación",
|
||
|
|
sharedAssetIds: [],
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal((await request(`${path}/${reusable.id}/delete`, {})).status, 200);
|
||
|
|
const concurrent = await create({ ...values, name: "Puesto concurrente", sharedAssetIds: [] });
|
||
|
|
const candidate = { ...values, name: "Nueva reserva concurrente", sharedAssetIds: [] };
|
||
|
|
const [retirement, reservation] = await Promise.all([
|
||
|
|
request(`${path}/${concurrent.id}/decommission`, {}),
|
||
|
|
request(path, { values: candidate }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
assert.equal(retirement.status, 200);
|
||
|
|
assert.ok([201, 409].includes(reservation.status));
|
||
|
|
const newStation =
|
||
|
|
reservation.status === 201
|
||
|
|
? WorkstationResponseSchema.parse(await reservation.json()).workstation
|
||
|
|
: await create(candidate);
|
||
|
|
|
||
|
|
assert.equal((await request(`${path}/${newStation.id}/delete`, {})).status, 200);
|
||
|
|
assert.equal((await request(`${path}/${concurrent.id}/delete`, {})).status, 200);
|
||
|
|
}
|