metis-desk/apps/server/src/workstation-lifecycle.integration.ts

166 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import {
AssetResponseSchema,
InventoryResponseSchema,
WorkstationResponseSchema,
} from "@metis/assets/common";
import { CompanyResponseSchema } from "@metis/companies/common";
import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
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 de baja"]) {
const response = await request(`/assets/${companyId}`, {
values: { ...source.values, name, status: "operational" },
});
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, 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.equal(retired.history.length, 2);
assert.deepEqual(retired.history[0]?.values.assetIds, [computer.id, shared.id]);
assert.deepEqual(
WorkstationResponseSchema.parse(await (await request(retirePath, {})).json()).workstation,
retired
);
// Los activos de la composición pueden reservarse de nuevo tras la baja.
const replacement = await create({
...values,
name: "Puesto reutiliza activos",
assetIds: [computer.id, shared.id],
});
assert.equal((await request(`${path}/${replacement.id}/delete`, {})).status, 200);
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" });
assert.equal((await request(`${path}/${direct.id}/delete`, {})).status, 200);
const reusable = await create({
...values,
name: "Reserva después de eliminación",
});
assert.equal((await request(`${path}/${reusable.id}/delete`, {})).status, 200);
const concurrent = await create({ ...values, name: "Puesto concurrente" });
const candidate = { ...values, name: "Nueva reserva concurrente" };
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);
}