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; 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); }